@@ -1,5 +1,33 @@ |
||
1 | 1 |
source 'https://rubygems.org' |
2 | 2 |
|
3 |
+# Optional libraries. To conserve RAM, comment out any that you don't need, |
|
4 |
+# then run `bundle` and commit the updated Gemfile and Gemfile.lock. |
|
5 |
+gem 'twilio-ruby', '~> 3.11.5' # TwilioAgent |
|
6 |
+gem 'ruby-growl', '~> 4.1.0' # GrowlAgent |
|
7 |
+gem 'net-ftp-list', '~> 3.2.8' # FtpsiteAgent |
|
8 |
+gem 'wunderground', '~> 1.2.0' # WeatherAgent |
|
9 |
+gem 'forecast_io', '~> 2.0.0' # WeatherAgent |
|
10 |
+gem 'rturk', '~> 2.12.1' # HumanTaskAgent |
|
11 |
+gem 'weibo_2', '~> 0.1.4' # Weibo Agents |
|
12 |
+gem 'hipchat', '~> 1.2.0' # HipchatAgent |
|
13 |
+gem 'xmpp4r', '~> 0.5.6' # JabberAgent |
|
14 |
+gem "google-api-client" # GoogleCalendarPublishAgent |
|
15 |
+gem 'mqtt' # MQTTAgent |
|
16 |
+gem 'slack-notifier', '~> 0.5.0' # SlackAgent |
|
17 |
+ |
|
18 |
+# Twitter Agents |
|
19 |
+gem 'twitter', '~> 5.8.0' # Must to be loaded before cantino-twitter-stream. |
|
20 |
+gem 'cantino-twitter-stream', github: 'cantino/twitter-stream', branch: 'master' |
|
21 |
+gem 'omniauth-twitter' |
|
22 |
+ |
|
23 |
+# Tumblr Agents |
|
24 |
+gem 'tumblr_client' |
|
25 |
+gem 'omniauth-tumblr' |
|
26 |
+ |
|
27 |
+# Optional Services. |
|
28 |
+gem 'omniauth-37signals' # BasecampAgent |
|
29 |
+# gem 'omniauth-github' |
|
30 |
+ |
|
3 | 31 |
# Bundler <1.5 does not recognize :x64_mingw as a valid platform name. |
4 | 32 |
# Unfortunately, it can't self-update because it errors when encountering :x64_mingw. |
5 | 33 |
unless Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('1.5.0') |
@@ -7,111 +35,66 @@ unless Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('1.5.0') |
||
7 | 35 |
exit 1 |
8 | 36 |
end |
9 | 37 |
|
10 |
-gem 'bundler', '>= 1.5.0' |
|
11 |
- |
|
12 |
-gem 'protected_attributes', '~>1.0.8' |
|
13 |
- |
|
14 |
-gem 'rails' , '4.1.5' |
|
15 |
- |
|
16 |
-case RUBY_PLATFORM |
|
17 |
-when /freebsd|netbsd|openbsd/ |
|
18 |
- # ffi (required by typhoeus via ethon) merged fixes for bugs fatal |
|
19 |
- # on these platforms after 1.9.3; no following release as yet. |
|
20 |
- gem 'ffi', github: 'ffi/ffi', branch: 'master' |
|
38 |
+gem 'protected_attributes', '~>1.0.8' # This must be loaded before some other gems, like delayed_job. |
|
21 | 39 |
|
22 |
- # tzinfo 1.2.0 has added support for reading zoneinfo on these |
|
23 |
- # platforms. |
|
24 |
- gem 'tzinfo', '>= 1.2.0' |
|
25 |
-when /solaris/ |
|
26 |
- # ditto |
|
27 |
- gem 'tzinfo', '>= 1.2.0' |
|
28 |
-end |
|
29 |
- |
|
30 |
-# Windows does not have zoneinfo files, so bundle the tzinfo-data gem. |
|
31 |
-gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw] |
|
32 |
- |
|
33 |
-gem 'mysql2', '~> 0.3.16' |
|
34 |
-gem 'devise', '~> 3.2.4' |
|
35 |
-gem 'kaminari', '~> 0.16.1' |
|
40 |
+gem 'ace-rails-ap', '~> 2.0.1' |
|
36 | 41 |
gem 'bootstrap-kaminari-views', '~> 0.0.3' |
37 |
-gem 'rufus-scheduler', '~> 3.0.8', require: false |
|
38 |
-gem 'json', '~> 1.8.1' |
|
39 |
-gem 'jsonpath', '~> 0.5.6' |
|
40 |
-gem 'twilio-ruby', '~> 3.11.5' |
|
41 |
-gem 'ruby-growl', '~> 4.1.0' |
|
42 |
-gem 'liquid', '~> 2.6.1' |
|
43 |
- |
|
42 |
+gem 'bundler', '>= 1.5.0' |
|
43 |
+gem 'coffee-rails', '~> 4.0.0' |
|
44 |
+gem 'daemons', '~> 1.1.9' |
|
44 | 45 |
gem 'delayed_job', '~> 4.0.0' |
45 | 46 |
gem 'delayed_job_active_record', '~> 4.0.0' |
46 |
-gem 'daemons', '~> 1.1.9' |
|
47 |
- |
|
47 |
+gem 'devise', '~> 3.2.4' |
|
48 |
+gem 'em-http-request', '~> 1.1.2' |
|
49 |
+gem 'faraday', '~> 0.9.0' |
|
50 |
+gem 'faraday_middleware' |
|
51 |
+gem 'feed-normalizer' |
|
52 |
+gem 'font-awesome-sass' |
|
48 | 53 |
gem 'foreman', '~> 0.63.0' |
49 |
- |
|
50 |
-gem 'sass-rails', '~> 4.0.0' |
|
51 |
-gem 'coffee-rails', '~> 4.0.0' |
|
52 |
-gem 'uglifier', '>= 1.3.0' |
|
53 |
-gem 'select2-rails', '~> 3.5.4' |
|
54 |
-gem 'jquery-rails', '~> 3.1.0' |
|
55 |
-gem 'ace-rails-ap', '~> 2.0.1' |
|
56 |
-gem 'spectrum-rails' |
|
57 |
- |
|
58 |
- |
|
59 | 54 |
# geokit-rails doesn't work with geokit 1.8.X but it specifies ~> 1.5 |
60 | 55 |
# in its own Gemfile. |
61 | 56 |
gem 'geokit', '~> 1.8.4' |
62 | 57 |
gem 'geokit-rails', '~> 2.0.1' |
63 |
- |
|
58 |
+gem 'httparty', '~> 0.13' |
|
59 |
+gem 'jquery-rails', '~> 3.1.0' |
|
60 |
+gem 'json', '~> 1.8.1' |
|
61 |
+gem 'jsonpath', '~> 0.5.6' |
|
62 |
+gem 'kaminari', '~> 0.16.1' |
|
64 | 63 |
gem 'kramdown', '~> 1.3.3' |
65 |
-gem 'faraday', '~> 0.9.0' |
|
66 |
-gem 'faraday_middleware' |
|
67 |
-gem 'typhoeus', '~> 0.6.3' |
|
64 |
+gem 'liquid', '~> 2.6.1' |
|
65 |
+gem 'mysql2', '~> 0.3.16' |
|
66 |
+gem 'multi_xml' |
|
68 | 67 |
gem 'nokogiri', '~> 1.6.1' |
69 |
-gem 'net-ftp-list', '~> 3.2.8' |
|
70 |
- |
|
71 |
-gem 'wunderground', '~> 1.2.0' |
|
72 |
-gem 'forecast_io', '~> 2.0.0' |
|
73 |
-gem 'rturk', '~> 2.12.1' |
|
74 |
- |
|
75 |
-gem "google-api-client" |
|
76 |
- |
|
77 |
-gem 'twitter', '~> 5.8.0' |
|
78 |
-gem 'cantino-twitter-stream', github: 'cantino/twitter-stream', branch: 'master' |
|
79 |
-gem 'em-http-request', '~> 1.1.2' |
|
80 |
-gem 'weibo_2', '~> 0.1.4' |
|
81 |
-gem 'hipchat', '~> 1.2.0' |
|
82 |
-gem 'xmpp4r', '~> 0.5.6' |
|
83 |
-gem 'feed-normalizer' |
|
84 |
-gem 'slack-notifier', '~> 0.5.0' |
|
85 |
-gem 'therubyracer', '~> 0.12.1' |
|
86 |
-gem 'mqtt' |
|
87 |
-gem 'tumblr_client' |
|
88 |
- |
|
89 | 68 |
gem 'omniauth' |
90 |
-gem 'omniauth-twitter' |
|
91 |
-gem 'omniauth-37signals' |
|
92 |
-gem 'omniauth-github' |
|
93 |
-gem 'omniauth-tumblr' |
|
69 |
+gem 'rails' , '4.1.5' |
|
70 |
+gem 'rufus-scheduler', '~> 3.0.8', require: false |
|
71 |
+gem 'sass-rails', '~> 4.0.0' |
|
72 |
+gem 'select2-rails', '~> 3.5.4' |
|
73 |
+gem 'spectrum-rails' |
|
74 |
+gem 'therubyracer', '~> 0.12.1' |
|
75 |
+gem 'typhoeus', '~> 0.6.3' |
|
76 |
+gem 'uglifier', '>= 1.3.0' |
|
94 | 77 |
|
95 | 78 |
group :development do |
96 |
- gem 'binding_of_caller' |
|
97 | 79 |
gem 'better_errors', '~> 1.1' |
80 |
+ gem 'binding_of_caller' |
|
98 | 81 |
gem 'quiet_assets' |
99 | 82 |
end |
100 | 83 |
|
101 | 84 |
group :development, :test do |
102 |
- gem 'vcr' |
|
85 |
+ gem 'coveralls', require: false |
|
86 |
+ gem 'delorean' |
|
103 | 87 |
gem 'dotenv-rails' |
104 | 88 |
gem 'pry' |
105 |
- gem 'rspec-rails', '~> 2.99' |
|
89 |
+ gem 'rr' |
|
106 | 90 |
gem 'rspec', '~> 2.99' |
107 | 91 |
gem 'rspec-collection_matchers' |
92 |
+ gem 'rspec-rails', '~> 2.99' |
|
108 | 93 |
gem 'shoulda-matchers' |
109 |
- gem 'rr' |
|
110 |
- gem 'delorean' |
|
111 |
- gem 'webmock', '~> 1.17.4', require: false |
|
112 |
- gem 'coveralls', require: false |
|
113 | 94 |
gem 'spring' |
114 | 95 |
gem 'spring-commands-rspec' |
96 |
+ gem 'vcr' |
|
97 |
+ gem 'webmock', '~> 1.17.4', require: false |
|
115 | 98 |
end |
116 | 99 |
|
117 | 100 |
group :production do |
@@ -119,6 +102,12 @@ group :production do |
||
119 | 102 |
gem 'rack' |
120 | 103 |
end |
121 | 104 |
|
105 |
+# Platform requirements. |
|
106 |
+gem 'ffi', '>= 1.9.4' # required by typhoeus; 1.9.4 has fixes for *BSD. |
|
107 |
+gem 'tzinfo', '>= 1.2.0' # required by rails; 1.2.0 has support for *BSD and Solaris. |
|
108 |
+# Windows does not have zoneinfo files, so bundle the tzinfo-data gem. |
|
109 |
+gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw] |
|
110 |
+ |
|
122 | 111 |
# This hack needs some explanation. When on Heroku, use the pg, unicorn, and rails12factor gems. |
123 | 112 |
# When not on Heroku, we still want our Gemfile.lock to include these gems, so we scope them to |
124 | 113 |
# an unsupported platform. |
@@ -119,7 +119,9 @@ GEM |
||
119 | 119 |
feed-normalizer (1.5.2) |
120 | 120 |
hpricot (>= 0.6) |
121 | 121 |
simple-rss (>= 1.1) |
122 |
- ffi (1.9.3) |
|
122 |
+ ffi (1.9.5) |
|
123 |
+ font-awesome-sass (4.2.0) |
|
124 |
+ sass (~> 3.2) |
|
123 | 125 |
forecast_io (2.0.0) |
124 | 126 |
faraday |
125 | 127 |
hashie |
@@ -204,9 +206,6 @@ GEM |
||
204 | 206 |
omniauth-37signals (1.0.5) |
205 | 207 |
omniauth (~> 1.0) |
206 | 208 |
omniauth-oauth2 (~> 1.0) |
207 |
- omniauth-github (1.1.2) |
|
208 |
- omniauth (~> 1.0) |
|
209 |
- omniauth-oauth2 (~> 1.1) |
|
210 | 209 |
omniauth-oauth (1.0.1) |
211 | 210 |
oauth |
212 | 211 |
omniauth (~> 1.0) |
@@ -419,12 +418,15 @@ DEPENDENCIES |
||
419 | 418 |
faraday (~> 0.9.0) |
420 | 419 |
faraday_middleware |
421 | 420 |
feed-normalizer |
421 |
+ ffi (>= 1.9.4) |
|
422 |
+ font-awesome-sass |
|
422 | 423 |
forecast_io (~> 2.0.0) |
423 | 424 |
foreman (~> 0.63.0) |
424 | 425 |
geokit (~> 1.8.4) |
425 | 426 |
geokit-rails (~> 2.0.1) |
426 | 427 |
google-api-client |
427 | 428 |
hipchat (~> 1.2.0) |
429 |
+ httparty (~> 0.13) |
|
428 | 430 |
jquery-rails (~> 3.1.0) |
429 | 431 |
json (~> 1.8.1) |
430 | 432 |
jsonpath (~> 0.5.6) |
@@ -432,13 +434,12 @@ DEPENDENCIES |
||
432 | 434 |
kramdown (~> 1.3.3) |
433 | 435 |
liquid (~> 2.6.1) |
434 | 436 |
mqtt |
437 |
+ multi_xml |
|
435 | 438 |
mysql2 (~> 0.3.16) |
436 | 439 |
net-ftp-list (~> 3.2.8) |
437 | 440 |
nokogiri (~> 1.6.1) |
438 | 441 |
omniauth |
439 | 442 |
omniauth-37signals |
440 |
- omniauth-github |
|
441 |
- omniauth-tumblr |
|
442 | 443 |
omniauth-twitter |
443 | 444 |
pg |
444 | 445 |
protected_attributes (~> 1.0.8) |
@@ -466,6 +467,7 @@ DEPENDENCIES |
||
466 | 467 |
twilio-ruby (~> 3.11.5) |
467 | 468 |
twitter (~> 5.8.0) |
468 | 469 |
typhoeus (~> 0.6.3) |
470 |
+ tzinfo (>= 1.2.0) |
|
469 | 471 |
tzinfo-data |
470 | 472 |
uglifier (>= 1.3.0) |
471 | 473 |
unicorn |
@@ -55,6 +55,7 @@ If you just want to play around, you can simply fork this repository, then perfo |
||
55 | 55 |
|
56 | 56 |
* Run `git remote add upstream https://github.com/cantino/huginn.git` to add the main repository as a remote for your fork. |
57 | 57 |
* Copy `.env.example` to `.env` (`cp .env.example .env`) and edit `.env`, at least updating the `APP_SECRET_TOKEN` variable. |
58 |
+* Run `bundle` to install dependencies |
|
58 | 59 |
* Run `rake db:create`, `rake db:migrate`, and then `rake db:seed` to create a development MySQL database with some example Agents. |
59 | 60 |
* Run `foreman start`, visit [http://localhost:3000/][localhost], and login with the username of `admin` and the password of `password`. |
60 | 61 |
* Setup some Agents! |
@@ -0,0 +1,12 @@ |
||
1 |
+#= require jquery |
|
2 |
+#= require jquery_ujs |
|
3 |
+#= require typeahead.bundle |
|
4 |
+#= require bootstrap |
|
5 |
+#= require select2 |
|
6 |
+#= require json2 |
|
7 |
+#= require jquery.json-editor |
|
8 |
+#= require latlon_and_geo |
|
9 |
+#= require spectrum |
|
10 |
+#= require_tree ./components |
|
11 |
+#= require_tree ./pages |
|
12 |
+#= require_self |
@@ -1,226 +0,0 @@ |
||
1 |
-#= require jquery |
|
2 |
-#= require jquery_ujs |
|
3 |
-#= require typeahead.bundle |
|
4 |
-#= require bootstrap |
|
5 |
-#= require select2 |
|
6 |
-#= require json2 |
|
7 |
-#= require jquery.json-editor |
|
8 |
-#= require latlon_and_geo |
|
9 |
-#= require spectrum |
|
10 |
-#= require ./worker-checker |
|
11 |
-#= require_self |
|
12 |
- |
|
13 |
-window.setupJsonEditor = ($editors = $(".live-json-editor")) -> |
|
14 |
- JSONEditor.prototype.ADD_IMG = '<%= image_path 'json-editor/add.png' %>' |
|
15 |
- JSONEditor.prototype.DELETE_IMG = '<%= image_path 'json-editor/delete.png' %>' |
|
16 |
- editors = [] |
|
17 |
- $editors.each -> |
|
18 |
- $editor = $(this) |
|
19 |
- jsonEditor = new JSONEditor($editor, $editor.data('width') || 400, $editor.data('height') || 500) |
|
20 |
- jsonEditor.doTruncation true |
|
21 |
- jsonEditor.showFunctionButtons() |
|
22 |
- editors.push jsonEditor |
|
23 |
- return editors |
|
24 |
- |
|
25 |
-hideSchedule = -> |
|
26 |
- $(".schedule-region .can-be-scheduled").hide() |
|
27 |
- $(".schedule-region .cannot-be-scheduled").show() |
|
28 |
- |
|
29 |
-showSchedule = (defaultSchedule = null) -> |
|
30 |
- if defaultSchedule? |
|
31 |
- $(".schedule-region select").val(defaultSchedule).change() |
|
32 |
- $(".schedule-region .can-be-scheduled").show() |
|
33 |
- $(".schedule-region .cannot-be-scheduled").hide() |
|
34 |
- |
|
35 |
-hideLinks = -> |
|
36 |
- $(".link-region .select2-container").hide() |
|
37 |
- $(".link-region .propagate-immediately").hide() |
|
38 |
- $(".link-region .cannot-receive-events").show() |
|
39 |
- |
|
40 |
-showLinks = -> |
|
41 |
- $(".link-region .select2-container").show() |
|
42 |
- $(".link-region .propagate-immediately").show() |
|
43 |
- $(".link-region .cannot-receive-events").hide() |
|
44 |
- showEventDescriptions() |
|
45 |
- |
|
46 |
-hideControlLinks = -> |
|
47 |
- $(".control-link-region").hide() |
|
48 |
- |
|
49 |
-showControlLinks = -> |
|
50 |
- $(".control-link-region").show() |
|
51 |
- |
|
52 |
-hideEventCreation = -> |
|
53 |
- $(".event-related-region").hide() |
|
54 |
- |
|
55 |
-showEventCreation = -> |
|
56 |
- $(".event-related-region").show() |
|
57 |
- |
|
58 |
-showEventDescriptions = -> |
|
59 |
- if $("#agent_source_ids").val() |
|
60 |
- $.getJSON "/agents/event_descriptions", { ids: $("#agent_source_ids").val().join(",") }, (json) => |
|
61 |
- if json.description_html? |
|
62 |
- $(".event-descriptions").show().html(json.description_html) |
|
63 |
- else |
|
64 |
- $(".event-descriptions").hide() |
|
65 |
- else |
|
66 |
- $(".event-descriptions").html("").hide() |
|
67 |
- |
|
68 |
-$(document).ready -> |
|
69 |
- $('.navbar .dropdown.dropdown-hover').hover \ |
|
70 |
- -> $(this).addClass('open'), |
|
71 |
- -> $(this).removeClass('open') |
|
72 |
- |
|
73 |
- # JSON Editor |
|
74 |
- window.jsonEditor = setupJsonEditor()[0] |
|
75 |
- |
|
76 |
- # Flash |
|
77 |
- if $(".flash").length |
|
78 |
- setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000) |
|
79 |
- |
|
80 |
- # Help popovers |
|
81 |
- $('.hover-help').popover(trigger: 'hover', html: true) |
|
82 |
- |
|
83 |
- # Agent Navigation |
|
84 |
- $agentNavigate = $('#agent-navigate') |
|
85 |
- |
|
86 |
- # initialize typeahead listener |
|
87 |
- $agentNavigate.bind "typeahead:selected", (event, object, name) -> |
|
88 |
- item = object['value'] |
|
89 |
- $agentNavigate.typeahead('val', '') |
|
90 |
- if agentPaths[item] |
|
91 |
- $(".spinner").show() |
|
92 |
- navigationData = agentPaths[item] |
|
93 |
- if !(navigationData instanceof Object) || !navigationData.method || navigationData.method == 'GET' |
|
94 |
- window.location = navigationData.url || navigationData |
|
95 |
- else |
|
96 |
- $("<a href='#{navigationData.url}' data-method='#{navigationData.method}'></a>").appendTo($("body")).click() |
|
97 |
- |
|
98 |
- # substring matcher for typeahead |
|
99 |
- substringMatcher = (strings)-> |
|
100 |
- findMatches = (query, callback) -> |
|
101 |
- matches = [] |
|
102 |
- substrRegex = new RegExp(query, "i") |
|
103 |
- $.each strings, (i, str) -> |
|
104 |
- matches.push value: str if substrRegex.test(str) |
|
105 |
- callback(matches.slice(0,6)) |
|
106 |
- |
|
107 |
- $agentNavigate.typeahead |
|
108 |
- minLength: 1, |
|
109 |
- highlight: true, |
|
110 |
- , |
|
111 |
- source: substringMatcher(agentNames) |
|
112 |
- |
|
113 |
- |
|
114 |
- # Pressing '/' selects the search box. |
|
115 |
- $("body").on "keypress", (e) -> |
|
116 |
- if e.keyCode == 47 # The '/' key |
|
117 |
- if e.target.nodeName == "BODY" |
|
118 |
- e.preventDefault() |
|
119 |
- $agentNavigate.focus() |
|
120 |
- |
|
121 |
- # Agent Show |
|
122 |
- fetchLogs = (e) -> |
|
123 |
- agentId = $(e.target).closest("[data-agent-id]").data("agent-id") |
|
124 |
- e.preventDefault() |
|
125 |
- $("#logs .spinner").show() |
|
126 |
- $("#logs .refresh, #logs .clear").hide() |
|
127 |
- $.get "/agents/#{agentId}/logs", (html) => |
|
128 |
- $("#logs .logs").html html |
|
129 |
- $("#logs .spinner").stop(true, true).fadeOut -> |
|
130 |
- $("#logs .refresh, #logs .clear").show() |
|
131 |
- |
|
132 |
- clearLogs = (e) -> |
|
133 |
- if confirm("Are you sure you want to clear all logs for this Agent?") |
|
134 |
- agentId = $(e.target).closest("[data-agent-id]").data("agent-id") |
|
135 |
- e.preventDefault() |
|
136 |
- $("#logs .spinner").show() |
|
137 |
- $("#logs .refresh, #logs .clear").hide() |
|
138 |
- $.post "/agents/#{agentId}/logs/clear", { "_method": "DELETE" }, (html) => |
|
139 |
- $("#logs .logs").html html |
|
140 |
- $("#show-tabs li a.recent-errors").removeClass 'recent-errors' |
|
141 |
- $("#logs .spinner").stop(true, true).fadeOut -> |
|
142 |
- $("#logs .refresh, #logs .clear").show() |
|
143 |
- |
|
144 |
- $(".agent-show #show-tabs a[href='#logs'], #logs .refresh").on "click", fetchLogs |
|
145 |
- $(".agent-show #logs .clear").on "click", clearLogs |
|
146 |
- |
|
147 |
- if tab = window.location.href.match(/tab=(\w+)\b/i)?[1] |
|
148 |
- if tab in ["details", "logs"] |
|
149 |
- $(".agent-show .nav-pills li a[href='##{tab}']").click() |
|
150 |
- |
|
151 |
- # Editing Agents |
|
152 |
- $("#agent_source_ids").on "change", showEventDescriptions |
|
153 |
- |
|
154 |
- $("#agent_type").on "change", -> |
|
155 |
- if window.jsonEditor? |
|
156 |
- $("#agent-spinner").fadeIn(); |
|
157 |
- $("#agent_source_ids").select2("val", {}); |
|
158 |
- $(".event-descriptions").html("").hide() |
|
159 |
- $.getJSON "/agents/type_details", { type: $(@).val() }, (json) => |
|
160 |
- if json.can_be_scheduled |
|
161 |
- showSchedule(json.default_schedule) |
|
162 |
- else |
|
163 |
- hideSchedule() |
|
164 |
- |
|
165 |
- if json.can_receive_events |
|
166 |
- showLinks() |
|
167 |
- else |
|
168 |
- hideLinks() |
|
169 |
- |
|
170 |
- if json.can_control_other_agents |
|
171 |
- showControlLinks() |
|
172 |
- else |
|
173 |
- hideControlLinks() |
|
174 |
- |
|
175 |
- if json.can_create_events |
|
176 |
- showEventCreation() |
|
177 |
- else |
|
178 |
- hideEventCreation() |
|
179 |
- |
|
180 |
- $(".description").html(json.description_html) if json.description_html? |
|
181 |
- |
|
182 |
- $('.oauthable-form').html(json.form) if json.form? |
|
183 |
- |
|
184 |
- if $("#agent_options").hasClass("showing-default") || $("#agent_options").val().match(/\A\s*(\{\s*\}|)\s*\Z/g) |
|
185 |
- window.jsonEditor.json = json.options |
|
186 |
- window.jsonEditor.rebuild() |
|
187 |
- |
|
188 |
- $("#agent-spinner").stop(true, true).fadeOut(); |
|
189 |
- |
|
190 |
- $("#agent_type").change() if $("#agent_type").length |
|
191 |
- |
|
192 |
- # Select2 Selects |
|
193 |
- $(".select2").select2(width: 'resolve') |
|
194 |
- |
|
195 |
- if $(".schedule-region") |
|
196 |
- if $(".schedule-region").data("can-be-scheduled") == true |
|
197 |
- showSchedule() |
|
198 |
- else |
|
199 |
- hideSchedule() |
|
200 |
- |
|
201 |
- if $(".link-region") |
|
202 |
- if $(".link-region").data("can-receive-events") == true |
|
203 |
- showLinks() |
|
204 |
- else |
|
205 |
- hideLinks() |
|
206 |
- |
|
207 |
- if $(".control-link-region") |
|
208 |
- if $(".control-link-region").data("can-control-other-agents") == true |
|
209 |
- showControlLinks() |
|
210 |
- else |
|
211 |
- hideControlLinks() |
|
212 |
- |
|
213 |
- if $(".event-related-region") |
|
214 |
- if $(".event-related-region").data("can-create-events") == true |
|
215 |
- showEventCreation() |
|
216 |
- else |
|
217 |
- hideEventCreation() |
|
218 |
- |
|
219 |
- $('.selectable-text').each -> |
|
220 |
- $(this).click -> |
|
221 |
- range = document.createRange() |
|
222 |
- range.setStartBefore(this.firstChild) |
|
223 |
- range.setEndAfter(this.lastChild) |
|
224 |
- sel = window.getSelection() |
|
225 |
- sel.removeAllRanges(); |
|
226 |
- sel.addRange(range) |
@@ -0,0 +1,30 @@ |
||
1 |
+$ -> |
|
2 |
+ # Flash |
|
3 |
+ if $(".flash").length |
|
4 |
+ setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000) |
|
5 |
+ |
|
6 |
+ # Help popovers |
|
7 |
+ $('.hover-help').popover(trigger: 'hover', html: true) |
|
8 |
+ |
|
9 |
+ # Pressing '/' selects the search box. |
|
10 |
+ $("body").on "keypress", (e) -> |
|
11 |
+ if e.keyCode == 47 # The '/' key |
|
12 |
+ if e.target.nodeName == "BODY" |
|
13 |
+ e.preventDefault() |
|
14 |
+ $agentNavigate.focus() |
|
15 |
+ |
|
16 |
+ # Select2 Selects |
|
17 |
+ $(".select2").select2(width: 'resolve') |
|
18 |
+ |
|
19 |
+ # Helper for selecting text when clicked |
|
20 |
+ $('.selectable-text').each -> |
|
21 |
+ $(this).click -> |
|
22 |
+ range = document.createRange() |
|
23 |
+ range.setStartBefore(this.firstChild) |
|
24 |
+ range.setEndAfter(this.lastChild) |
|
25 |
+ sel = window.getSelection() |
|
26 |
+ sel.removeAllRanges(); |
|
27 |
+ sel.addRange(range) |
|
28 |
+ |
|
29 |
+ # Agent navbar dropdown |
|
30 |
+ $('.navbar .dropdown.dropdown-hover').hover (-> $(this).addClass('open')), (-> $(this).removeClass('open')) |
@@ -0,0 +1,14 @@ |
||
1 |
+window.setupJsonEditor = ($editors = $(".live-json-editor")) -> |
|
2 |
+ JSONEditor.prototype.ADD_IMG = '<%= image_path 'json-editor/add.png' %>' |
|
3 |
+ JSONEditor.prototype.DELETE_IMG = '<%= image_path 'json-editor/delete.png' %>' |
|
4 |
+ editors = [] |
|
5 |
+ $editors.each -> |
|
6 |
+ $editor = $(this) |
|
7 |
+ jsonEditor = new JSONEditor($editor, $editor.data('width') || 400, $editor.data('height') || 500) |
|
8 |
+ jsonEditor.doTruncation true |
|
9 |
+ jsonEditor.showFunctionButtons() |
|
10 |
+ editors.push jsonEditor |
|
11 |
+ return editors |
|
12 |
+ |
|
13 |
+$ -> |
|
14 |
+ window.jsonEditor = setupJsonEditor()[0] |
@@ -0,0 +1,29 @@ |
||
1 |
+$ -> |
|
2 |
+ $agentNavigate = $('#agent-navigate') |
|
3 |
+ |
|
4 |
+ # initialize typeahead listener |
|
5 |
+ $agentNavigate.bind "typeahead:selected", (event, object, name) -> |
|
6 |
+ item = object['value'] |
|
7 |
+ $agentNavigate.typeahead('val', '') |
|
8 |
+ if window.agentPaths[item] |
|
9 |
+ $(".spinner").show() |
|
10 |
+ navigationData = window.agentPaths[item] |
|
11 |
+ if !(navigationData instanceof Object) || !navigationData.method || navigationData.method == 'GET' |
|
12 |
+ window.location = navigationData.url || navigationData |
|
13 |
+ else |
|
14 |
+ $("<a href='#{navigationData.url}' data-method='#{navigationData.method}'></a>").appendTo($("body")).click() |
|
15 |
+ |
|
16 |
+ # substring matcher for typeahead |
|
17 |
+ substringMatcher = (strings) -> |
|
18 |
+ findMatches = (query, callback) -> |
|
19 |
+ matches = [] |
|
20 |
+ substrRegex = new RegExp(query, "i") |
|
21 |
+ $.each strings, (i, str) -> |
|
22 |
+ matches.push value: str if substrRegex.test(str) |
|
23 |
+ callback(matches.slice(0,6)) |
|
24 |
+ |
|
25 |
+ $agentNavigate.typeahead |
|
26 |
+ minLength: 1, |
|
27 |
+ highlight: true, |
|
28 |
+ , |
|
29 |
+ source: substringMatcher(window.agentNames) |
@@ -0,0 +1,14 @@ |
||
1 |
+class @Utils |
|
2 |
+ @navigatePath: (path) -> |
|
3 |
+ path = "/" + path unless path.match(/^\//) |
|
4 |
+ window.location.href = path |
|
5 |
+ |
|
6 |
+ @currentPath: -> |
|
7 |
+ window.location.href.replace(/https?:\/\/.*?\//g, '') |
|
8 |
+ |
|
9 |
+ @registerPage: (klass, options = {}) -> |
|
10 |
+ if options.forPathsMatching? |
|
11 |
+ if Utils.currentPath().match(options.forPathsMatching) |
|
12 |
+ window.currentPage = new klass() |
|
13 |
+ else |
|
14 |
+ new klass() |
@@ -1,3 +1,5 @@ |
||
1 |
+# This is not included in the core application.js bundle. |
|
2 |
+ |
|
1 | 3 |
$ -> |
2 | 4 |
svg = document.querySelector('.agent-diagram svg.diagram') |
3 | 5 |
overlay = document.querySelector('.agent-diagram .overlay') |
@@ -2,6 +2,8 @@ |
||
2 | 2 |
#= require rickshaw |
3 | 3 |
#= require_self |
4 | 4 |
|
5 |
+# This is not included in the core application.js bundle. |
|
6 |
+ |
|
5 | 7 |
window.renderGraph = ($chart, data, peaks, name) -> |
6 | 8 |
graph = new Rickshaw.Graph |
7 | 9 |
element: $chart.find(".chart").get(0) |
@@ -0,0 +1,41 @@ |
||
1 |
+window.map_marker = (map, options = {}) -> |
|
2 |
+ pos = new google.maps.LatLng(options.lat, options.lng) |
|
3 |
+ |
|
4 |
+ if options.radius > 0 |
|
5 |
+ new google.maps.Circle |
|
6 |
+ map: map |
|
7 |
+ strokeColor: '#FF0000' |
|
8 |
+ strokeOpacity: 0.8 |
|
9 |
+ strokeWeight: 2 |
|
10 |
+ fillColor: '#FF0000' |
|
11 |
+ fillOpacity: 0.35 |
|
12 |
+ center: pos |
|
13 |
+ radius: options.radius |
|
14 |
+ else |
|
15 |
+ new google.maps.Marker |
|
16 |
+ map: map |
|
17 |
+ position: pos |
|
18 |
+ title: 'Recorded Location' |
|
19 |
+ |
|
20 |
+ if options.course |
|
21 |
+ p1 = new LatLon(pos.lat(), pos.lng()) |
|
22 |
+ speed = options.speed ? 1 |
|
23 |
+ p2 = p1.destinationPoint(options.course, Math.max(0.2, speed) * 0.1) |
|
24 |
+ |
|
25 |
+ lineCoordinates = [ |
|
26 |
+ pos |
|
27 |
+ new google.maps.LatLng(p2.lat(), p2.lon()) |
|
28 |
+ ] |
|
29 |
+ |
|
30 |
+ lineSymbol = |
|
31 |
+ path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW |
|
32 |
+ |
|
33 |
+ new google.maps.Polyline |
|
34 |
+ map: map |
|
35 |
+ path: lineCoordinates |
|
36 |
+ icons: [ |
|
37 |
+ { |
|
38 |
+ icon: lineSymbol |
|
39 |
+ offset: '100%' |
|
40 |
+ } |
|
41 |
+ ] |
@@ -0,0 +1,126 @@ |
||
1 |
+class @AgentEditPage |
|
2 |
+ constructor: -> |
|
3 |
+ $("#agent_source_ids").on "change", @showEventDescriptions |
|
4 |
+ @showCorrectRegionsOnStartup() |
|
5 |
+ |
|
6 |
+ # The type selector is only available on the new agent form. |
|
7 |
+ if $("#agent_type").length |
|
8 |
+ $("#agent_type").on "change", => @handleTypeChange(false) |
|
9 |
+ @handleTypeChange(true) |
|
10 |
+ |
|
11 |
+ handleTypeChange: (firstTime) -> |
|
12 |
+ $(".event-descriptions").html("").hide() |
|
13 |
+ type = $('#agent_type').val() |
|
14 |
+ |
|
15 |
+ if type == 'Agent' |
|
16 |
+ $(".agent-settings").hide() |
|
17 |
+ $(".description").hide() |
|
18 |
+ else |
|
19 |
+ $(".agent-settings").show() |
|
20 |
+ $("#agent-spinner").fadeIn() |
|
21 |
+ $("#agent_source_ids").select2("val", {}) |
|
22 |
+ $(".model-errors").hide() unless firstTime |
|
23 |
+ $.getJSON "/agents/type_details", { type: type }, (json) => |
|
24 |
+ if json.can_be_scheduled |
|
25 |
+ if firstTime |
|
26 |
+ @showSchedule() |
|
27 |
+ else |
|
28 |
+ @showSchedule(json.default_schedule) |
|
29 |
+ else |
|
30 |
+ @hideSchedule() |
|
31 |
+ |
|
32 |
+ if json.can_receive_events |
|
33 |
+ @showLinks() |
|
34 |
+ else |
|
35 |
+ @hideLinks() |
|
36 |
+ |
|
37 |
+ if json.can_control_other_agents |
|
38 |
+ @showControlLinks() |
|
39 |
+ else |
|
40 |
+ @hideControlLinks() |
|
41 |
+ |
|
42 |
+ if json.can_create_events |
|
43 |
+ @showEventCreation() |
|
44 |
+ else |
|
45 |
+ @hideEventCreation() |
|
46 |
+ |
|
47 |
+ $(".description").show().html(json.description_html) if json.description_html? |
|
48 |
+ |
|
49 |
+ $('.oauthable-form').html(json.form) if json.form? |
|
50 |
+ |
|
51 |
+ unless firstTime |
|
52 |
+ window.jsonEditor.json = json.options |
|
53 |
+ window.jsonEditor.rebuild() |
|
54 |
+ |
|
55 |
+ $("#agent-spinner").stop(true, true).fadeOut(); |
|
56 |
+ |
|
57 |
+ hideSchedule: -> |
|
58 |
+ $(".schedule-region .can-be-scheduled").hide() |
|
59 |
+ $(".schedule-region .cannot-be-scheduled").show() |
|
60 |
+ |
|
61 |
+ showSchedule: (defaultSchedule = null) -> |
|
62 |
+ if defaultSchedule? |
|
63 |
+ $(".schedule-region select").val(defaultSchedule).change() |
|
64 |
+ $(".schedule-region .can-be-scheduled").show() |
|
65 |
+ $(".schedule-region .cannot-be-scheduled").hide() |
|
66 |
+ |
|
67 |
+ hideLinks: -> |
|
68 |
+ $(".link-region .select2-container").hide() |
|
69 |
+ $(".link-region .propagate-immediately").hide() |
|
70 |
+ $(".link-region .cannot-receive-events").show() |
|
71 |
+ |
|
72 |
+ showLinks: -> |
|
73 |
+ $(".link-region .select2-container").show() |
|
74 |
+ $(".link-region .propagate-immediately").show() |
|
75 |
+ $(".link-region .cannot-receive-events").hide() |
|
76 |
+ @showEventDescriptions() |
|
77 |
+ |
|
78 |
+ hideControlLinks: -> |
|
79 |
+ $(".control-link-region").hide() |
|
80 |
+ |
|
81 |
+ showControlLinks: -> |
|
82 |
+ $(".control-link-region").show() |
|
83 |
+ |
|
84 |
+ hideEventCreation: -> |
|
85 |
+ $(".event-related-region").hide() |
|
86 |
+ |
|
87 |
+ showEventCreation: -> |
|
88 |
+ $(".event-related-region").show() |
|
89 |
+ |
|
90 |
+ showEventDescriptions: -> |
|
91 |
+ if $("#agent_source_ids").val() |
|
92 |
+ $.getJSON "/agents/event_descriptions", { ids: $("#agent_source_ids").val().join(",") }, (json) => |
|
93 |
+ if json.description_html? |
|
94 |
+ $(".event-descriptions").show().html(json.description_html) |
|
95 |
+ else |
|
96 |
+ $(".event-descriptions").hide() |
|
97 |
+ else |
|
98 |
+ $(".event-descriptions").html("").hide() |
|
99 |
+ |
|
100 |
+ showCorrectRegionsOnStartup: -> |
|
101 |
+ if $(".schedule-region") |
|
102 |
+ if $(".schedule-region").data("can-be-scheduled") == true |
|
103 |
+ @showSchedule() |
|
104 |
+ else |
|
105 |
+ @hideSchedule() |
|
106 |
+ |
|
107 |
+ if $(".link-region") |
|
108 |
+ if $(".link-region").data("can-receive-events") == true |
|
109 |
+ @showLinks() |
|
110 |
+ else |
|
111 |
+ @hideLinks() |
|
112 |
+ |
|
113 |
+ if $(".control-link-region") |
|
114 |
+ if $(".control-link-region").data("can-control-other-agents") == true |
|
115 |
+ @showControlLinks() |
|
116 |
+ else |
|
117 |
+ @hideControlLinks() |
|
118 |
+ |
|
119 |
+ if $(".event-related-region") |
|
120 |
+ if $(".event-related-region").data("can-create-events") == true |
|
121 |
+ @showEventCreation() |
|
122 |
+ else |
|
123 |
+ @hideEventCreation() |
|
124 |
+ |
|
125 |
+$ -> |
|
126 |
+ Utils.registerPage(AgentEditPage, forPathsMatching: /^agents/) |
@@ -0,0 +1,35 @@ |
||
1 |
+class @AgentShowPage |
|
2 |
+ constructor: -> |
|
3 |
+ $(".agent-show #show-tabs a[href='#logs'], #logs .refresh").on "click", @fetchLogs |
|
4 |
+ $(".agent-show #logs .clear").on "click", @clearLogs |
|
5 |
+ |
|
6 |
+ # Trigger tabs when navigated to. |
|
7 |
+ if tab = window.location.href.match(/tab=(\w+)\b/i)?[1] |
|
8 |
+ if tab in ["details", "logs"] |
|
9 |
+ $(".agent-show .nav-pills li a[href='##{tab}']").click() |
|
10 |
+ |
|
11 |
+ fetchLogs: (e) -> |
|
12 |
+ agentId = $(e.target).closest("[data-agent-id]").data("agent-id") |
|
13 |
+ e.preventDefault() |
|
14 |
+ $("#logs .spinner").show() |
|
15 |
+ $("#logs .refresh, #logs .clear").hide() |
|
16 |
+ $.get "/agents/#{agentId}/logs", (html) => |
|
17 |
+ $("#logs .logs").html html |
|
18 |
+ $("#logs .spinner").stop(true, true).fadeOut -> |
|
19 |
+ $("#logs .refresh, #logs .clear").show() |
|
20 |
+ |
|
21 |
+ clearLogs: (e) -> |
|
22 |
+ if confirm("Are you sure you want to clear all logs for this Agent?") |
|
23 |
+ agentId = $(e.target).closest("[data-agent-id]").data("agent-id") |
|
24 |
+ e.preventDefault() |
|
25 |
+ $("#logs .spinner").show() |
|
26 |
+ $("#logs .refresh, #logs .clear").hide() |
|
27 |
+ $.post "/agents/#{agentId}/logs/clear", { "_method": "DELETE" }, (html) => |
|
28 |
+ $("#logs .logs").html html |
|
29 |
+ $("#show-tabs li a.recent-errors").removeClass 'recent-errors' |
|
30 |
+ $("#logs .spinner").stop(true, true).fadeOut -> |
|
31 |
+ $("#logs .refresh, #logs .clear").show() |
|
32 |
+ |
|
33 |
+$ -> |
|
34 |
+ Utils.registerPage(AgentShowPage, forPathsMatching: /^agents\/\d+/) |
|
35 |
+ |
@@ -3,6 +3,8 @@ |
||
3 | 3 |
#= require ace/mode-markdown.js |
4 | 4 |
#= require_self |
5 | 5 |
|
6 |
+# This is not included in the core application.js bundle. |
|
7 |
+ |
|
6 | 8 |
$ -> |
7 | 9 |
editor = ace.edit("ace-credential-value") |
8 | 10 |
editor.getSession().setTabSize(2) |
@@ -18,6 +18,8 @@ |
||
18 | 18 |
*/ |
19 | 19 |
|
20 | 20 |
@import "bootstrap"; |
21 |
+@import "font-awesome-sprockets"; |
|
22 |
+@import "font-awesome"; |
|
21 | 23 |
|
22 | 24 |
body { padding-top: 60px; } |
23 | 25 |
|
@@ -86,6 +88,11 @@ span.not-applicable:after { |
||
86 | 88 |
.nav > li { |
87 | 89 |
&.job-indicator, &#event-indicator { |
88 | 90 |
display: none; |
91 |
+ |
|
92 |
+ a { |
|
93 |
+ padding-right: 5px; |
|
94 |
+ padding-left: 5px; |
|
95 |
+ } |
|
89 | 96 |
} |
90 | 97 |
} |
91 | 98 |
|
@@ -170,7 +177,7 @@ span.not-applicable:after { |
||
170 | 177 |
|
171 | 178 |
// Disabled |
172 | 179 |
|
173 |
-.agent-disabled { |
|
180 |
+.agent-unavailable { |
|
174 | 181 |
opacity: 0.5; |
175 | 182 |
} |
176 | 183 |
|
@@ -232,3 +239,38 @@ h2 .scenario, a span.label.scenario { |
||
232 | 239 |
.confirm-agent .popover { |
233 | 240 |
width: 200px; |
234 | 241 |
} |
242 |
+ |
|
243 |
+.btn-auth { |
|
244 |
+ position: relative; |
|
245 |
+ padding-left: 40px; |
|
246 |
+ $border-color: rgba(0,0,0,0.2); |
|
247 |
+ border-color: $border-color; |
|
248 |
+ |
|
249 |
+ > i:first-child { |
|
250 |
+ position: absolute; |
|
251 |
+ top: 0; |
|
252 |
+ left: 0; |
|
253 |
+ bottom: 0; |
|
254 |
+ width: 32px; |
|
255 |
+ height: 32px; |
|
256 |
+ text-align: center; |
|
257 |
+ line-height: 32px; |
|
258 |
+ font-size: 24px; |
|
259 |
+ border-right: 1px solid $border-color; |
|
260 |
+ } |
|
261 |
+ |
|
262 |
+ &.btn-auth-twitter { |
|
263 |
+ color: #fff; |
|
264 |
+ background-color: #55acee; |
|
265 |
+ } |
|
266 |
+ |
|
267 |
+ &.btn-auth-37signals { |
|
268 |
+ color: #fff; |
|
269 |
+ background-color: #8fc857; |
|
270 |
+ } |
|
271 |
+ |
|
272 |
+ &.btn-auth-github { |
|
273 |
+ color: #fff; |
|
274 |
+ background-color: #444; |
|
275 |
+ } |
|
276 |
+} |
@@ -6,17 +6,17 @@ |
||
6 | 6 |
&.asc:after, &.desc:after { |
7 | 7 |
text-decoration: none; |
8 | 8 |
position: absolute; |
9 |
- top: -5px; |
|
10 |
- right: -12px; |
|
11 |
- font-size: 1.2em; |
|
9 |
+ top: 0; |
|
10 |
+ right: -1em; |
|
11 |
+ font-family: FontAwesome; |
|
12 | 12 |
} |
13 | 13 |
|
14 | 14 |
&.asc:after { |
15 |
- content: '\2193'; |
|
15 |
+ content: '\f0de'; //fa-sort-asc |
|
16 | 16 |
} |
17 | 17 |
|
18 | 18 |
&.desc:after { |
19 |
- content: '\2191'; |
|
19 |
+ content: '\f0dd'; //fa-sort-desc |
|
20 | 20 |
} |
21 | 21 |
} |
22 | 22 |
|
@@ -5,7 +5,9 @@ module TwitterConcern |
||
5 | 5 |
include Oauthable |
6 | 6 |
|
7 | 7 |
validate :validate_twitter_options |
8 |
- valid_oauth_providers :twitter |
|
8 |
+ valid_oauth_providers 'twitter' |
|
9 |
+ |
|
10 |
+ gem_dependency_check { defined?(Twitter) && has_oauth_configuration_for?('twitter') } |
|
9 | 11 |
end |
10 | 12 |
|
11 | 13 |
def validate_twitter_options |
@@ -41,4 +43,10 @@ module TwitterConcern |
||
41 | 43 |
config.access_token_secret = twitter_oauth_token_secret |
42 | 44 |
end |
43 | 45 |
end |
46 |
+ |
|
47 |
+ module ClassMethods |
|
48 |
+ def twitter_dependencies_missing |
|
49 |
+ "## Include the `twitter`, `omniauth-twitter`, and `cantino-twitter-stream` gems in your Gemfile to use Twitter Agents." |
|
50 |
+ end |
|
51 |
+ end |
|
44 | 52 |
end |
@@ -2,6 +2,8 @@ module WeiboConcern |
||
2 | 2 |
extend ActiveSupport::Concern |
3 | 3 |
|
4 | 4 |
included do |
5 |
+ gem_dependency_check { defined?(WeiboOAuth2) } |
|
6 |
+ |
|
5 | 7 |
self.validate :validate_weibo_options |
6 | 8 |
end |
7 | 9 |
|
@@ -22,8 +24,4 @@ module WeiboConcern |
||
22 | 24 |
end |
23 | 25 |
@weibo_client |
24 | 26 |
end |
25 |
- |
|
26 |
- module ClassMethods |
|
27 |
- |
|
28 |
- end |
|
29 | 27 |
end |
@@ -43,7 +43,7 @@ class AgentsController < ApplicationController |
||
43 | 43 |
:can_control_other_agents => @agent.can_control_other_agents?, |
44 | 44 |
:options => @agent.default_options, |
45 | 45 |
:description_html => @agent.html_description, |
46 |
- :form => render_to_string(partial: 'oauth_dropdown') |
|
46 |
+ :form => render_to_string(partial: 'oauth_dropdown', locals: { agent: @agent }) |
|
47 | 47 |
} |
48 | 48 |
end |
49 | 49 |
|
@@ -8,7 +8,9 @@ class UserCredentialsController < ApplicationController |
||
8 | 8 |
|
9 | 9 |
respond_to do |format| |
10 | 10 |
format.html |
11 |
- format.json { render json: @user_credentials } |
|
11 |
+ format.json { |
|
12 |
+ send_data Utils.pretty_jsonify(@user_credentials.limit(nil).as_json), disposition: 'attachment' |
|
13 |
+ } |
|
12 | 14 |
end |
13 | 15 |
end |
14 | 16 |
|
@@ -32,6 +32,8 @@ module ApplicationHelper |
||
32 | 32 |
def working(agent) |
33 | 33 |
if agent.disabled? |
34 | 34 |
link_to 'Disabled', agent_path(agent), class: 'label label-warning' |
35 |
+ elsif agent.dependencies_missing? |
|
36 |
+ content_tag :span, 'Missing Gems', class: 'label label-danger' |
|
35 | 37 |
elsif agent.working? |
36 | 38 |
content_tag :span, 'Yes', class: 'label label-success' |
37 | 39 |
else |
@@ -137,9 +137,9 @@ module DotHelper |
||
137 | 137 |
label: agent_label[agent], |
138 | 138 |
tooltip: (agent.short_type.titleize if rich), |
139 | 139 |
URL: (agent_url[agent] if rich), |
140 |
- style: ('rounded,dashed' if agent.disabled?), |
|
141 |
- color: (@disabled if agent.disabled?), |
|
142 |
- fontcolor: (@disabled if agent.disabled?)) |
|
140 |
+ style: ('rounded,dashed' if agent.unavailable?), |
|
141 |
+ color: (@disabled if agent.unavailable?), |
|
142 |
+ fontcolor: (@disabled if agent.unavailable?)) |
|
143 | 143 |
end |
144 | 144 |
|
145 | 145 |
def agent_edge(agent, receiver) |
@@ -148,7 +148,7 @@ module DotHelper |
||
148 | 148 |
style: ('dashed' unless receiver.propagate_immediately?), |
149 | 149 |
label: (" #{agent.control_action}s " if agent.can_control_other_agents?), |
150 | 150 |
arrowhead: ('empty' if agent.can_control_other_agents?), |
151 |
- color: (@disabled if agent.disabled? || receiver.disabled?)) |
|
151 |
+ color: (@disabled if agent.unavailable? || receiver.unavailable?)) |
|
152 | 152 |
end |
153 | 153 |
|
154 | 154 |
block('digraph', 'Agent Event Flow') { |
@@ -218,7 +218,7 @@ module DotHelper |
||
218 | 218 |
# a dummy label only to obtain the background color |
219 | 219 |
label['class'] = [ |
220 | 220 |
'label', |
221 |
- if agent.disabled? |
|
221 |
+ if agent.unavailable? |
|
222 | 222 |
'label-warning' |
223 | 223 |
elsif agent.working? |
224 | 224 |
'label-success' |
@@ -1,5 +0,0 @@ |
||
1 |
-module ServiceHelper |
|
2 |
- def has_oauth_configuration_for(provider) |
|
3 |
- ENV["#{provider.upcase}_OAUTH_KEY"].present? && ENV["#{provider.upcase}_OAUTH_SECRET"].present? |
|
4 |
- end |
|
5 |
-end |
@@ -150,6 +150,14 @@ class Agent < ActiveRecord::Base |
||
150 | 150 |
end |
151 | 151 |
end |
152 | 152 |
|
153 |
+ def unavailable? |
|
154 |
+ disabled? || dependencies_missing? |
|
155 |
+ end |
|
156 |
+ |
|
157 |
+ def dependencies_missing? |
|
158 |
+ self.class.dependencies_missing? |
|
159 |
+ end |
|
160 |
+ |
|
153 | 161 |
def default_schedule |
154 | 162 |
self.class.default_schedule |
155 | 163 |
end |
@@ -317,6 +325,15 @@ class Agent < ActiveRecord::Base |
||
317 | 325 |
include? AgentControllerConcern |
318 | 326 |
end |
319 | 327 |
|
328 |
+ def gem_dependency_check |
|
329 |
+ @gem_dependencies_checked = true |
|
330 |
+ @gem_dependencies_met = yield |
|
331 |
+ end |
|
332 |
+ |
|
333 |
+ def dependencies_missing? |
|
334 |
+ @gem_dependencies_checked && !@gem_dependencies_met |
|
335 |
+ end |
|
336 |
+ |
|
320 | 337 |
# Find all Agents that have received Events since the last execution of this method. Update those Agents with |
321 | 338 |
# their new `last_checked_event_id` and queue each of the Agents to be called with #receive using `async_receive`. |
322 | 339 |
# This is called by bin/schedule.rb periodically. |
@@ -362,7 +379,7 @@ class Agent < ActiveRecord::Base |
||
362 | 379 |
def async_receive(agent_id, event_ids) |
363 | 380 |
agent = Agent.find(agent_id) |
364 | 381 |
begin |
365 |
- return if agent.disabled? |
|
382 |
+ return if agent.unavailable? |
|
366 | 383 |
agent.receive(Event.where(:id => event_ids)) |
367 | 384 |
agent.last_receive_at = Time.now |
368 | 385 |
agent.save! |
@@ -400,7 +417,7 @@ class Agent < ActiveRecord::Base |
||
400 | 417 |
def async_check(agent_id) |
401 | 418 |
agent = Agent.find(agent_id) |
402 | 419 |
begin |
403 |
- return if agent.disabled? |
|
420 |
+ return if agent.unavailable? |
|
404 | 421 |
agent.check |
405 | 422 |
agent.last_check_at = Time.now |
406 | 423 |
agent.save! |
@@ -1,6 +1,5 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class AdiosoAgent < Agent |
3 |
- |
|
4 | 3 |
cannot_receive_events! |
5 | 4 |
|
6 | 5 |
default_schedule "every_1d" |
@@ -1,15 +1,15 @@ |
||
1 |
-require 'net/ftp' |
|
2 |
-require 'net/ftp/list' |
|
3 | 1 |
require 'uri' |
4 | 2 |
require 'time' |
5 | 3 |
|
6 | 4 |
module Agents |
7 | 5 |
class FtpsiteAgent < Agent |
8 | 6 |
cannot_receive_events! |
9 |
- |
|
10 | 7 |
default_schedule "every_12h" |
11 | 8 |
|
9 |
+ gem_dependency_check { defined?(Net::FTP) && defined?(Net::FTP::List) } |
|
10 |
+ |
|
12 | 11 |
description <<-MD |
12 |
+ #{'## Include `net-ftp-list` in your Gemfile to use this Agent!' if dependencies_missing?} |
|
13 | 13 |
The FtpsiteAgent checks a FTP site and creates Events based on newly uploaded files in a directory. |
14 | 14 |
|
15 | 15 |
Specify a `url` that represents a directory of an FTP site to watch, and a list of `patterns` to match against file names. |
@@ -35,12 +35,12 @@ module Agents |
||
35 | 35 |
|
36 | 36 |
def default_options |
37 | 37 |
{ |
38 |
- 'expected_update_period_in_days' => "1", |
|
39 |
- 'url' => "ftp://example.org/pub/releases/", |
|
40 |
- 'patterns' => [ |
|
41 |
- 'foo-*.tar.gz', |
|
42 |
- ], |
|
43 |
- 'after' => Time.now.iso8601, |
|
38 |
+ 'expected_update_period_in_days' => "1", |
|
39 |
+ 'url' => "ftp://example.org/pub/releases/", |
|
40 |
+ 'patterns' => [ |
|
41 |
+ 'foo-*.tar.gz', |
|
42 |
+ ], |
|
43 |
+ 'after' => Time.now.iso8601, |
|
44 | 44 |
} |
45 | 45 |
end |
46 | 46 |
|
@@ -4,7 +4,10 @@ module Agents |
||
4 | 4 |
class GoogleCalendarPublishAgent < Agent |
5 | 5 |
cannot_be_scheduled! |
6 | 6 |
|
7 |
+ gem_dependency_check { defined?(GoogleCalendar) } |
|
8 |
+ |
|
7 | 9 |
description <<-MD |
10 |
+ #{'## Include `google-api-client` in your Gemfile to use this Agent!' if dependencies_missing?} |
|
8 | 11 |
The GoogleCalendarPublishAgent creates events on your google calendar. |
9 | 12 |
|
10 | 13 |
This agent relies on service accounts, rather than oauth. |
@@ -1,5 +1,3 @@ |
||
1 |
-require 'ruby-growl' |
|
2 |
- |
|
3 | 1 |
module Agents |
4 | 2 |
class GrowlAgent < Agent |
5 | 3 |
attr_reader :growler |
@@ -7,7 +5,10 @@ module Agents |
||
7 | 5 |
cannot_be_scheduled! |
8 | 6 |
cannot_create_events! |
9 | 7 |
|
8 |
+ gem_dependency_check { defined?(Growl) } |
|
9 |
+ |
|
10 | 10 |
description <<-MD |
11 |
+ #{'## Include `ruby-growl` in your Gemfile to use this Agent!' if dependencies_missing?} |
|
11 | 12 |
The GrowlAgent sends any events it receives to a Growl GNTP server immediately. |
12 | 13 |
|
13 | 14 |
It is assumed that events have a `message` or `text` key, which will hold the body of the growl notification, and a `subject` key, which will have the headline of the Growl notification. You can use Event Formatting Agent if your event does not provide these keys. |
@@ -34,13 +35,13 @@ module Agents |
||
34 | 35 |
errors.add(:base, "growl_server and expected_receive_period_in_days are required fields") |
35 | 36 |
end |
36 | 37 |
end |
37 |
- |
|
38 |
+ |
|
38 | 39 |
def register_growl |
39 | 40 |
@growler = Growl.new interpolated['growl_server'], interpolated['growl_app_name'], "GNTP" |
40 | 41 |
@growler.password = interpolated['growl_password'] |
41 | 42 |
@growler.add_notification interpolated['growl_notification_name'] |
42 | 43 |
end |
43 |
- |
|
44 |
+ |
|
44 | 45 |
def notify_growl(subject, message) |
45 | 46 |
@growler.notify(interpolated['growl_notification_name'], subject, message) |
46 | 47 |
end |
@@ -3,7 +3,10 @@ module Agents |
||
3 | 3 |
cannot_be_scheduled! |
4 | 4 |
cannot_create_events! |
5 | 5 |
|
6 |
+ gem_dependency_check { defined?(HipChat) } |
|
7 |
+ |
|
6 | 8 |
description <<-MD |
9 |
+ #{'## Include `hipchat` in your Gemfile to use this Agent!' if dependencies_missing?} |
|
7 | 10 |
The HipchatAgent sends messages to a Hipchat Room |
8 | 11 |
|
9 | 12 |
To authenticate you need to set the `auth_token`, you can get one at your Hipchat Group Admin page which you can find here: |
@@ -40,11 +43,14 @@ module Agents |
||
40 | 43 |
end |
41 | 44 |
|
42 | 45 |
def receive(incoming_events) |
43 |
- client = HipChat::Client.new(interpolated[:auth_token] || credential('hipchat_auth_token')) |
|
44 | 46 |
incoming_events.each do |event| |
45 | 47 |
mo = interpolated(event) |
46 | 48 |
client[mo[:room_name]].send(mo[:username][0..14], mo[:message], :notify => boolify(mo[:notify]), :color => mo[:color]) |
47 | 49 |
end |
48 | 50 |
end |
51 |
+ |
|
52 |
+ def client |
|
53 |
+ @client ||= HipChat::Client.new(interpolated[:auth_token] || credential('hipchat_auth_token')) |
|
54 |
+ end |
|
49 | 55 |
end |
50 | 56 |
end |
@@ -1,10 +1,11 @@ |
||
1 |
-require 'rturk' |
|
2 |
- |
|
3 | 1 |
module Agents |
4 | 2 |
class HumanTaskAgent < Agent |
5 | 3 |
default_schedule "every_10m" |
6 | 4 |
|
5 |
+ gem_dependency_check { defined?(RTurk) } |
|
6 |
+ |
|
7 | 7 |
description <<-MD |
8 |
+ #{'## Include `rturk` in your Gemfile to use this Agent!' if dependencies_missing?} |
|
8 | 9 |
You can use a HumanTaskAgent to create Human Intelligence Tasks (HITs) on Mechanical Turk. |
9 | 10 |
|
10 | 11 |
HITs can be created in response to events, or on a schedule. Set `trigger_on` to either `schedule` or `event`. |
@@ -226,266 +227,269 @@ module Agents |
||
226 | 227 |
|
227 | 228 |
protected |
228 | 229 |
|
229 |
- def take_majority? |
|
230 |
- interpolated['combination_mode'] == "take_majority" || interpolated['take_majority'] == "true" |
|
231 |
- end |
|
230 |
+ if defined?(RTurk) |
|
232 | 231 |
|
233 |
- def create_poll? |
|
234 |
- interpolated['combination_mode'] == "poll" |
|
235 |
- end |
|
232 |
+ def take_majority? |
|
233 |
+ interpolated['combination_mode'] == "take_majority" || interpolated['take_majority'] == "true" |
|
234 |
+ end |
|
236 | 235 |
|
237 |
- def event_for_hit(hit_id) |
|
238 |
- if memory['hits'][hit_id].is_a?(Hash) |
|
239 |
- Event.find_by_id(memory['hits'][hit_id]['event_id']) |
|
240 |
- else |
|
241 |
- nil |
|
236 |
+ def create_poll? |
|
237 |
+ interpolated['combination_mode'] == "poll" |
|
242 | 238 |
end |
243 |
- end |
|
244 | 239 |
|
245 |
- def hit_type(hit_id) |
|
246 |
- if memory['hits'][hit_id].is_a?(Hash) && memory['hits'][hit_id]['type'] |
|
247 |
- memory['hits'][hit_id]['type'] |
|
248 |
- else |
|
249 |
- 'user' |
|
240 |
+ def event_for_hit(hit_id) |
|
241 |
+ if memory['hits'][hit_id].is_a?(Hash) |
|
242 |
+ Event.find_by_id(memory['hits'][hit_id]['event_id']) |
|
243 |
+ else |
|
244 |
+ nil |
|
245 |
+ end |
|
250 | 246 |
end |
251 |
- end |
|
252 | 247 |
|
253 |
- def review_hits |
|
254 |
- reviewable_hit_ids = RTurk::GetReviewableHITs.create.hit_ids |
|
255 |
- my_reviewed_hit_ids = reviewable_hit_ids & (memory['hits'] || {}).keys |
|
256 |
- if reviewable_hit_ids.length > 0 |
|
257 |
- log "MTurk reports #{reviewable_hit_ids.length} HITs, of which I own [#{my_reviewed_hit_ids.to_sentence}]" |
|
248 |
+ def hit_type(hit_id) |
|
249 |
+ if memory['hits'][hit_id].is_a?(Hash) && memory['hits'][hit_id]['type'] |
|
250 |
+ memory['hits'][hit_id]['type'] |
|
251 |
+ else |
|
252 |
+ 'user' |
|
253 |
+ end |
|
258 | 254 |
end |
259 | 255 |
|
260 |
- my_reviewed_hit_ids.each do |hit_id| |
|
261 |
- hit = RTurk::Hit.new(hit_id) |
|
262 |
- assignments = hit.assignments |
|
256 |
+ def review_hits |
|
257 |
+ reviewable_hit_ids = RTurk::GetReviewableHITs.create.hit_ids |
|
258 |
+ my_reviewed_hit_ids = reviewable_hit_ids & (memory['hits'] || {}).keys |
|
259 |
+ if reviewable_hit_ids.length > 0 |
|
260 |
+ log "MTurk reports #{reviewable_hit_ids.length} HITs, of which I own [#{my_reviewed_hit_ids.to_sentence}]" |
|
261 |
+ end |
|
262 |
+ |
|
263 |
+ my_reviewed_hit_ids.each do |hit_id| |
|
264 |
+ hit = RTurk::Hit.new(hit_id) |
|
265 |
+ assignments = hit.assignments |
|
263 | 266 |
|
264 |
- log "Looking at HIT #{hit_id}. I found #{assignments.length} assignments#{" with the statuses: #{assignments.map(&:status).to_sentence}" if assignments.length > 0}" |
|
265 |
- if assignments.length == hit.max_assignments && assignments.all? { |assignment| assignment.status == "Submitted" } |
|
266 |
- inbound_event = event_for_hit(hit_id) |
|
267 |
+ log "Looking at HIT #{hit_id}. I found #{assignments.length} assignments#{" with the statuses: #{assignments.map(&:status).to_sentence}" if assignments.length > 0}" |
|
268 |
+ if assignments.length == hit.max_assignments && assignments.all? { |assignment| assignment.status == "Submitted" } |
|
269 |
+ inbound_event = event_for_hit(hit_id) |
|
267 | 270 |
|
268 |
- if hit_type(hit_id) == 'poll' |
|
269 |
- # handle completed polls |
|
271 |
+ if hit_type(hit_id) == 'poll' |
|
272 |
+ # handle completed polls |
|
270 | 273 |
|
271 |
- log "Handling a poll: #{hit_id}" |
|
274 |
+ log "Handling a poll: #{hit_id}" |
|
272 | 275 |
|
273 |
- scores = {} |
|
274 |
- assignments.each do |assignment| |
|
275 |
- assignment.answers.each do |index, rating| |
|
276 |
- scores[index] ||= 0 |
|
277 |
- scores[index] += rating.to_i |
|
276 |
+ scores = {} |
|
277 |
+ assignments.each do |assignment| |
|
278 |
+ assignment.answers.each do |index, rating| |
|
279 |
+ scores[index] ||= 0 |
|
280 |
+ scores[index] += rating.to_i |
|
281 |
+ end |
|
278 | 282 |
end |
279 |
- end |
|
280 | 283 |
|
281 |
- top_answer = scores.to_a.sort {|b, a| a.last <=> b.last }.first.first |
|
284 |
+ top_answer = scores.to_a.sort {|b, a| a.last <=> b.last }.first.first |
|
282 | 285 |
|
283 |
- payload = { |
|
284 |
- 'answers' => memory['hits'][hit_id]['answers'], |
|
285 |
- 'poll' => assignments.map(&:answers), |
|
286 |
- 'best_answer' => memory['hits'][hit_id]['answers'][top_answer.to_i - 1] |
|
287 |
- } |
|
286 |
+ payload = { |
|
287 |
+ 'answers' => memory['hits'][hit_id]['answers'], |
|
288 |
+ 'poll' => assignments.map(&:answers), |
|
289 |
+ 'best_answer' => memory['hits'][hit_id]['answers'][top_answer.to_i - 1] |
|
290 |
+ } |
|
288 | 291 |
|
289 |
- event = create_event :payload => payload |
|
290 |
- log "Event emitted with answer(s) for poll", :outbound_event => event, :inbound_event => inbound_event |
|
291 |
- else |
|
292 |
- # handle normal completed HITs |
|
293 |
- payload = { 'answers' => assignments.map(&:answers) } |
|
294 |
- |
|
295 |
- if take_majority? |
|
296 |
- counts = {} |
|
297 |
- options['hit']['questions'].each do |question| |
|
298 |
- question_counts = question['selections'].inject({}) { |memo, selection| memo[selection['key']] = 0; memo } |
|
299 |
- assignments.each do |assignment| |
|
300 |
- answers = ActiveSupport::HashWithIndifferentAccess.new(assignment.answers) |
|
301 |
- answer = answers[question['key']] |
|
302 |
- question_counts[answer] += 1 |
|
292 |
+ event = create_event :payload => payload |
|
293 |
+ log "Event emitted with answer(s) for poll", :outbound_event => event, :inbound_event => inbound_event |
|
294 |
+ else |
|
295 |
+ # handle normal completed HITs |
|
296 |
+ payload = { 'answers' => assignments.map(&:answers) } |
|
297 |
+ |
|
298 |
+ if take_majority? |
|
299 |
+ counts = {} |
|
300 |
+ options['hit']['questions'].each do |question| |
|
301 |
+ question_counts = question['selections'].inject({}) { |memo, selection| memo[selection['key']] = 0; memo } |
|
302 |
+ assignments.each do |assignment| |
|
303 |
+ answers = ActiveSupport::HashWithIndifferentAccess.new(assignment.answers) |
|
304 |
+ answer = answers[question['key']] |
|
305 |
+ question_counts[answer] += 1 |
|
306 |
+ end |
|
307 |
+ counts[question['key']] = question_counts |
|
303 | 308 |
end |
304 |
- counts[question['key']] = question_counts |
|
305 |
- end |
|
306 |
- payload['counts'] = counts |
|
309 |
+ payload['counts'] = counts |
|
307 | 310 |
|
308 |
- majority_answer = counts.inject({}) do |memo, (key, question_counts)| |
|
309 |
- memo[key] = question_counts.to_a.sort {|a, b| a.last <=> b.last }.last.first |
|
310 |
- memo |
|
311 |
- end |
|
312 |
- payload['majority_answer'] = majority_answer |
|
313 |
- |
|
314 |
- if all_questions_are_numeric? |
|
315 |
- average_answer = counts.inject({}) do |memo, (key, question_counts)| |
|
316 |
- sum = divisor = 0 |
|
317 |
- question_counts.to_a.each do |num, count| |
|
318 |
- sum += num.to_s.to_f * count |
|
319 |
- divisor += count |
|
320 |
- end |
|
321 |
- memo[key] = sum / divisor.to_f |
|
311 |
+ majority_answer = counts.inject({}) do |memo, (key, question_counts)| |
|
312 |
+ memo[key] = question_counts.to_a.sort {|a, b| a.last <=> b.last }.last.first |
|
322 | 313 |
memo |
323 | 314 |
end |
324 |
- payload['average_answer'] = average_answer |
|
325 |
- end |
|
326 |
- end |
|
327 |
- |
|
328 |
- if create_poll? |
|
329 |
- questions = [] |
|
330 |
- selections = 5.times.map { |i| { 'key' => i+1, 'text' => i+1 } }.reverse |
|
331 |
- assignments.length.times do |index| |
|
332 |
- questions << { |
|
333 |
- 'type' => "selection", |
|
334 |
- 'name' => "Item #{index + 1}", |
|
335 |
- 'key' => index, |
|
336 |
- 'required' => "true", |
|
337 |
- 'question' => interpolate_string(options['poll_options']['row_template'], assignments[index].answers), |
|
338 |
- 'selections' => selections |
|
339 |
- } |
|
315 |
+ payload['majority_answer'] = majority_answer |
|
316 |
+ |
|
317 |
+ if all_questions_are_numeric? |
|
318 |
+ average_answer = counts.inject({}) do |memo, (key, question_counts)| |
|
319 |
+ sum = divisor = 0 |
|
320 |
+ question_counts.to_a.each do |num, count| |
|
321 |
+ sum += num.to_s.to_f * count |
|
322 |
+ divisor += count |
|
323 |
+ end |
|
324 |
+ memo[key] = sum / divisor.to_f |
|
325 |
+ memo |
|
326 |
+ end |
|
327 |
+ payload['average_answer'] = average_answer |
|
328 |
+ end |
|
340 | 329 |
end |
341 | 330 |
|
342 |
- poll_hit = create_hit 'title' => options['poll_options']['title'], |
|
343 |
- 'description' => options['poll_options']['instructions'], |
|
344 |
- 'questions' => questions, |
|
345 |
- 'assignments' => options['poll_options']['assignments'], |
|
346 |
- 'lifetime_in_seconds' => options['poll_options']['lifetime_in_seconds'], |
|
347 |
- 'reward' => options['poll_options']['reward'], |
|
348 |
- 'payload' => inbound_event && inbound_event.payload, |
|
349 |
- 'metadata' => { 'type' => 'poll', |
|
350 |
- 'original_hit' => hit_id, |
|
351 |
- 'answers' => assignments.map(&:answers), |
|
352 |
- 'event_id' => inbound_event && inbound_event.id } |
|
353 |
- |
|
354 |
- log "Poll HIT created with ID #{poll_hit.id} and URL #{poll_hit.url}. Original HIT: #{hit_id}", :inbound_event => inbound_event |
|
355 |
- else |
|
356 |
- if options[:separate_answers] |
|
357 |
- payload['answers'].each.with_index do |answer, index| |
|
358 |
- sub_payload = payload.dup |
|
359 |
- sub_payload.delete('answers') |
|
360 |
- sub_payload['answer'] = answer |
|
361 |
- event = create_event :payload => sub_payload |
|
362 |
- log "Event emitted with answer ##{index}", :outbound_event => event, :inbound_event => inbound_event |
|
331 |
+ if create_poll? |
|
332 |
+ questions = [] |
|
333 |
+ selections = 5.times.map { |i| { 'key' => i+1, 'text' => i+1 } }.reverse |
|
334 |
+ assignments.length.times do |index| |
|
335 |
+ questions << { |
|
336 |
+ 'type' => "selection", |
|
337 |
+ 'name' => "Item #{index + 1}", |
|
338 |
+ 'key' => index, |
|
339 |
+ 'required' => "true", |
|
340 |
+ 'question' => interpolate_string(options['poll_options']['row_template'], assignments[index].answers), |
|
341 |
+ 'selections' => selections |
|
342 |
+ } |
|
363 | 343 |
end |
344 |
+ |
|
345 |
+ poll_hit = create_hit 'title' => options['poll_options']['title'], |
|
346 |
+ 'description' => options['poll_options']['instructions'], |
|
347 |
+ 'questions' => questions, |
|
348 |
+ 'assignments' => options['poll_options']['assignments'], |
|
349 |
+ 'lifetime_in_seconds' => options['poll_options']['lifetime_in_seconds'], |
|
350 |
+ 'reward' => options['poll_options']['reward'], |
|
351 |
+ 'payload' => inbound_event && inbound_event.payload, |
|
352 |
+ 'metadata' => { 'type' => 'poll', |
|
353 |
+ 'original_hit' => hit_id, |
|
354 |
+ 'answers' => assignments.map(&:answers), |
|
355 |
+ 'event_id' => inbound_event && inbound_event.id } |
|
356 |
+ |
|
357 |
+ log "Poll HIT created with ID #{poll_hit.id} and URL #{poll_hit.url}. Original HIT: #{hit_id}", :inbound_event => inbound_event |
|
364 | 358 |
else |
365 |
- event = create_event :payload => payload |
|
366 |
- log "Event emitted with answer(s)", :outbound_event => event, :inbound_event => inbound_event |
|
359 |
+ if options[:separate_answers] |
|
360 |
+ payload['answers'].each.with_index do |answer, index| |
|
361 |
+ sub_payload = payload.dup |
|
362 |
+ sub_payload.delete('answers') |
|
363 |
+ sub_payload['answer'] = answer |
|
364 |
+ event = create_event :payload => sub_payload |
|
365 |
+ log "Event emitted with answer ##{index}", :outbound_event => event, :inbound_event => inbound_event |
|
366 |
+ end |
|
367 |
+ else |
|
368 |
+ event = create_event :payload => payload |
|
369 |
+ log "Event emitted with answer(s)", :outbound_event => event, :inbound_event => inbound_event |
|
370 |
+ end |
|
367 | 371 |
end |
368 | 372 |
end |
369 |
- end |
|
370 | 373 |
|
371 |
- assignments.each(&:approve!) |
|
372 |
- hit.dispose! |
|
374 |
+ assignments.each(&:approve!) |
|
375 |
+ hit.dispose! |
|
373 | 376 |
|
374 |
- memory['hits'].delete(hit_id) |
|
377 |
+ memory['hits'].delete(hit_id) |
|
378 |
+ end |
|
375 | 379 |
end |
376 | 380 |
end |
377 |
- end |
|
378 | 381 |
|
379 |
- def all_questions_are_numeric? |
|
380 |
- interpolated['hit']['questions'].all? do |question| |
|
381 |
- question['selections'].all? do |selection| |
|
382 |
- selection['key'] == selection['key'].to_f.to_s || selection['key'] == selection['key'].to_i.to_s |
|
382 |
+ def all_questions_are_numeric? |
|
383 |
+ interpolated['hit']['questions'].all? do |question| |
|
384 |
+ question['selections'].all? do |selection| |
|
385 |
+ selection['key'] == selection['key'].to_f.to_s || selection['key'] == selection['key'].to_i.to_s |
|
386 |
+ end |
|
383 | 387 |
end |
384 | 388 |
end |
385 |
- end |
|
386 |
- |
|
387 |
- def create_basic_hit(event = nil) |
|
388 |
- hit = create_hit 'title' => options['hit']['title'], |
|
389 |
- 'description' => options['hit']['description'], |
|
390 |
- 'questions' => options['hit']['questions'], |
|
391 |
- 'assignments' => options['hit']['assignments'], |
|
392 |
- 'lifetime_in_seconds' => options['hit']['lifetime_in_seconds'], |
|
393 |
- 'reward' => options['hit']['reward'], |
|
394 |
- 'payload' => event && event.payload, |
|
395 |
- 'metadata' => { 'event_id' => event && event.id } |
|
396 |
- |
|
397 |
- log "HIT created with ID #{hit.id} and URL #{hit.url}", :inbound_event => event |
|
398 |
- end |
|
399 | 389 |
|
400 |
- def create_hit(opts = {}) |
|
401 |
- payload = opts['payload'] || {} |
|
402 |
- title = interpolate_string(opts['title'], payload).strip |
|
403 |
- description = interpolate_string(opts['description'], payload).strip |
|
404 |
- questions = interpolate_options(opts['questions'], payload) |
|
405 |
- hit = RTurk::Hit.create(:title => title) do |hit| |
|
406 |
- hit.max_assignments = (opts['assignments'] || 1).to_i |
|
407 |
- hit.description = description |
|
408 |
- hit.lifetime = (opts['lifetime_in_seconds'] || 24 * 60 * 60).to_i |
|
409 |
- hit.question_form AgentQuestionForm.new(:title => title, :description => description, :questions => questions) |
|
410 |
- hit.reward = (opts['reward'] || 0.05).to_f |
|
411 |
- #hit.qualifications.add :approval_rate, { :gt => 80 } |
|
390 |
+ def create_basic_hit(event = nil) |
|
391 |
+ hit = create_hit 'title' => options['hit']['title'], |
|
392 |
+ 'description' => options['hit']['description'], |
|
393 |
+ 'questions' => options['hit']['questions'], |
|
394 |
+ 'assignments' => options['hit']['assignments'], |
|
395 |
+ 'lifetime_in_seconds' => options['hit']['lifetime_in_seconds'], |
|
396 |
+ 'reward' => options['hit']['reward'], |
|
397 |
+ 'payload' => event && event.payload, |
|
398 |
+ 'metadata' => { 'event_id' => event && event.id } |
|
399 |
+ |
|
400 |
+ log "HIT created with ID #{hit.id} and URL #{hit.url}", :inbound_event => event |
|
412 | 401 |
end |
413 |
- memory['hits'] ||= {} |
|
414 |
- memory['hits'][hit.id] = opts['metadata'] || {} |
|
415 |
- hit |
|
416 |
- end |
|
417 | 402 |
|
418 |
- # RTurk Question Form |
|
403 |
+ def create_hit(opts = {}) |
|
404 |
+ payload = opts['payload'] || {} |
|
405 |
+ title = interpolate_string(opts['title'], payload).strip |
|
406 |
+ description = interpolate_string(opts['description'], payload).strip |
|
407 |
+ questions = interpolate_options(opts['questions'], payload) |
|
408 |
+ hit = RTurk::Hit.create(:title => title) do |hit| |
|
409 |
+ hit.max_assignments = (opts['assignments'] || 1).to_i |
|
410 |
+ hit.description = description |
|
411 |
+ hit.lifetime = (opts['lifetime_in_seconds'] || 24 * 60 * 60).to_i |
|
412 |
+ hit.question_form AgentQuestionForm.new(:title => title, :description => description, :questions => questions) |
|
413 |
+ hit.reward = (opts['reward'] || 0.05).to_f |
|
414 |
+ #hit.qualifications.add :approval_rate, { :gt => 80 } |
|
415 |
+ end |
|
416 |
+ memory['hits'] ||= {} |
|
417 |
+ memory['hits'][hit.id] = opts['metadata'] || {} |
|
418 |
+ hit |
|
419 |
+ end |
|
419 | 420 |
|
420 |
- class AgentQuestionForm < RTurk::QuestionForm |
|
421 |
- needs :title, :description, :questions |
|
421 |
+ # RTurk Question Form |
|
422 | 422 |
|
423 |
- def question_form_content |
|
424 |
- Overview do |
|
425 |
- Title do |
|
426 |
- text @title |
|
427 |
- end |
|
428 |
- Text do |
|
429 |
- text @description |
|
430 |
- end |
|
431 |
- end |
|
423 |
+ class AgentQuestionForm < RTurk::QuestionForm |
|
424 |
+ needs :title, :description, :questions |
|
432 | 425 |
|
433 |
- @questions.each.with_index do |question, index| |
|
434 |
- Question do |
|
435 |
- QuestionIdentifier do |
|
436 |
- text question['key'] || "question_#{index}" |
|
426 |
+ def question_form_content |
|
427 |
+ Overview do |
|
428 |
+ Title do |
|
429 |
+ text @title |
|
437 | 430 |
end |
438 |
- DisplayName do |
|
439 |
- text question['name'] || "Question ##{index}" |
|
431 |
+ Text do |
|
432 |
+ text @description |
|
440 | 433 |
end |
441 |
- IsRequired do |
|
442 |
- text question['required'] || 'true' |
|
443 |
- end |
|
444 |
- QuestionContent do |
|
445 |
- Text do |
|
446 |
- text question['question'] |
|
434 |
+ end |
|
435 |
+ |
|
436 |
+ @questions.each.with_index do |question, index| |
|
437 |
+ Question do |
|
438 |
+ QuestionIdentifier do |
|
439 |
+ text question['key'] || "question_#{index}" |
|
447 | 440 |
end |
448 |
- end |
|
449 |
- AnswerSpecification do |
|
450 |
- if question['type'] == "selection" |
|
441 |
+ DisplayName do |
|
442 |
+ text question['name'] || "Question ##{index}" |
|
443 |
+ end |
|
444 |
+ IsRequired do |
|
445 |
+ text question['required'] || 'true' |
|
446 |
+ end |
|
447 |
+ QuestionContent do |
|
448 |
+ Text do |
|
449 |
+ text question['question'] |
|
450 |
+ end |
|
451 |
+ end |
|
452 |
+ AnswerSpecification do |
|
453 |
+ if question['type'] == "selection" |
|
451 | 454 |
|
452 |
- SelectionAnswer do |
|
453 |
- StyleSuggestion do |
|
454 |
- text 'radiobutton' |
|
455 |
- end |
|
456 |
- Selections do |
|
457 |
- question['selections'].each do |selection| |
|
458 |
- Selection do |
|
459 |
- SelectionIdentifier do |
|
460 |
- text selection['key'] |
|
461 |
- end |
|
462 |
- Text do |
|
463 |
- text selection['text'] |
|
455 |
+ SelectionAnswer do |
|
456 |
+ StyleSuggestion do |
|
457 |
+ text 'radiobutton' |
|
458 |
+ end |
|
459 |
+ Selections do |
|
460 |
+ question['selections'].each do |selection| |
|
461 |
+ Selection do |
|
462 |
+ SelectionIdentifier do |
|
463 |
+ text selection['key'] |
|
464 |
+ end |
|
465 |
+ Text do |
|
466 |
+ text selection['text'] |
|
467 |
+ end |
|
464 | 468 |
end |
465 | 469 |
end |
466 | 470 |
end |
467 | 471 |
end |
468 |
- end |
|
469 | 472 |
|
470 |
- else |
|
473 |
+ else |
|
471 | 474 |
|
472 |
- FreeTextAnswer do |
|
473 |
- if question['min_length'].present? || question['max_length'].present? |
|
474 |
- Constraints do |
|
475 |
- lengths = {} |
|
476 |
- lengths['minLength'] = question['min_length'].to_s if question['min_length'].present? |
|
477 |
- lengths['maxLength'] = question['max_length'].to_s if question['max_length'].present? |
|
478 |
- Length lengths |
|
475 |
+ FreeTextAnswer do |
|
476 |
+ if question['min_length'].present? || question['max_length'].present? |
|
477 |
+ Constraints do |
|
478 |
+ lengths = {} |
|
479 |
+ lengths['minLength'] = question['min_length'].to_s if question['min_length'].present? |
|
480 |
+ lengths['maxLength'] = question['max_length'].to_s if question['max_length'].present? |
|
481 |
+ Length lengths |
|
482 |
+ end |
|
479 | 483 |
end |
480 |
- end |
|
481 | 484 |
|
482 |
- if question['default'].present? |
|
483 |
- DefaultText do |
|
484 |
- text question['default'] |
|
485 |
+ if question['default'].present? |
|
486 |
+ DefaultText do |
|
487 |
+ text question['default'] |
|
488 |
+ end |
|
485 | 489 |
end |
486 | 490 |
end |
487 |
- end |
|
488 | 491 |
|
492 |
+ end |
|
489 | 493 |
end |
490 | 494 |
end |
491 | 495 |
end |
@@ -3,7 +3,10 @@ module Agents |
||
3 | 3 |
cannot_be_scheduled! |
4 | 4 |
cannot_create_events! |
5 | 5 |
|
6 |
+ gem_dependency_check { defined?(Jabber) } |
|
7 |
+ |
|
6 | 8 |
description <<-MD |
9 |
+ #{'## Include `xmpp4r` in your Gemfile to use this Agent!' if dependencies_missing?} |
|
7 | 10 |
The JabberAgent will send any events it receives to your Jabber/XMPP IM account. |
8 | 11 |
|
9 | 12 |
Specify the `jabber_server` and `jabber_port` for your Jabber server. |
@@ -1,10 +1,12 @@ |
||
1 | 1 |
# encoding: utf-8 |
2 |
-require "mqtt" |
|
3 | 2 |
require "json" |
4 | 3 |
|
5 | 4 |
module Agents |
6 | 5 |
class MqttAgent < Agent |
6 |
+ gem_dependency_check { defined?(MQTT) } |
|
7 |
+ |
|
7 | 8 |
description <<-MD |
9 |
+ #{'## Include `mqtt` in your Gemfile to use this Agent!' if dependencies_missing?} |
|
8 | 10 |
The MQTT agent allows both publication and subscription to an MQTT topic. |
9 | 11 |
|
10 | 12 |
MQTT is a generic transport protocol for machine to machine communication. |
@@ -1,11 +1,15 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class SlackAgent < Agent |
3 |
+ DEFAULT_WEBHOOK = 'incoming-webhook' |
|
4 |
+ DEFAULT_USERNAME = 'Huginn' |
|
5 |
+ |
|
3 | 6 |
cannot_be_scheduled! |
4 | 7 |
cannot_create_events! |
5 | 8 |
|
6 |
- DEFAULT_WEBHOOK = 'incoming-webhook' |
|
7 |
- DEFAULT_USERNAME = 'Huginn' |
|
9 |
+ gem_dependency_check { defined?(Slack) } |
|
10 |
+ |
|
8 | 11 |
description <<-MD |
12 |
+ #{'## Include `slack-notifier` in your Gemfile to use this Agent!' if dependencies_missing?} |
|
9 | 13 |
The SlackAgent lets you receive events and send notifications to [slack](https://slack.com/). |
10 | 14 |
|
11 | 15 |
To get started, you will first need to setup an incoming webhook. |
@@ -1,4 +1,3 @@ |
||
1 |
-require 'twilio-ruby' |
|
2 | 1 |
require 'securerandom' |
3 | 2 |
|
4 | 3 |
module Agents |
@@ -6,7 +5,10 @@ module Agents |
||
6 | 5 |
cannot_be_scheduled! |
7 | 6 |
cannot_create_events! |
8 | 7 |
|
8 |
+ gem_dependency_check { defined?(Twilio) } |
|
9 |
+ |
|
9 | 10 |
description <<-MD |
11 |
+ #{'## Include `twilio-ruby` in your Gemfile to use this Agent!' if dependencies_missing?} |
|
10 | 12 |
The TwilioAgent receives and collects events and sends them via text message (up to 160 characters) or gives you a call when scheduled. |
11 | 13 |
|
12 | 14 |
It is assumed that events have a `message`, `text`, or `sms` key, the value of which is sent as the content of the text message/call. You can use the EventFormattingAgent if your event does not provide these keys. |
@@ -39,7 +41,6 @@ module Agents |
||
39 | 41 |
end |
40 | 42 |
|
41 | 43 |
def receive(incoming_events) |
42 |
- @client = Twilio::REST::Client.new interpolated['account_sid'], interpolated['auth_token'] |
|
43 | 44 |
memory['pending_calls'] ||= {} |
44 | 45 |
incoming_events.each do |event| |
45 | 46 |
message = (event.payload['message'].presence || event.payload['text'].presence || event.payload['sms'].presence).to_s |
@@ -63,15 +64,15 @@ module Agents |
||
63 | 64 |
end |
64 | 65 |
|
65 | 66 |
def send_message(message) |
66 |
- @client.account.sms.messages.create :from => interpolated['sender_cell'], |
|
67 |
- :to => interpolated['receiver_cell'], |
|
68 |
- :body => message |
|
67 |
+ client.account.sms.messages.create :from => interpolated['sender_cell'], |
|
68 |
+ :to => interpolated['receiver_cell'], |
|
69 |
+ :body => message |
|
69 | 70 |
end |
70 | 71 |
|
71 | 72 |
def make_call(secret) |
72 |
- @client.account.calls.create :from => interpolated['sender_cell'], |
|
73 |
- :to => interpolated['receiver_cell'], |
|
74 |
- :url => post_url(interpolated['server_url'], secret) |
|
73 |
+ client.account.calls.create :from => interpolated['sender_cell'], |
|
74 |
+ :to => interpolated['receiver_cell'], |
|
75 |
+ :url => post_url(interpolated['server_url'], secret) |
|
75 | 76 |
end |
76 | 77 |
|
77 | 78 |
def post_url(server_url, secret) |
@@ -85,5 +86,9 @@ module Agents |
||
85 | 86 |
[response.text, 200] |
86 | 87 |
end |
87 | 88 |
end |
89 |
+ |
|
90 |
+ def client |
|
91 |
+ @client ||= Twilio::REST::Client.new interpolated['account_sid'], interpolated['auth_token'] |
|
92 |
+ end |
|
88 | 93 |
end |
89 | 94 |
end |
@@ -1,5 +1,3 @@ |
||
1 |
-require "twitter" |
|
2 |
- |
|
3 | 1 |
module Agents |
4 | 2 |
class TwitterPublishAgent < Agent |
5 | 3 |
include TwitterConcern |
@@ -7,6 +5,7 @@ module Agents |
||
7 | 5 |
cannot_be_scheduled! |
8 | 6 |
|
9 | 7 |
description <<-MD |
8 |
+ #{twitter_dependencies_missing if dependencies_missing?} |
|
10 | 9 |
The TwitterPublishAgent publishes tweets from the events it receives. |
11 | 10 |
|
12 | 11 |
To be able to use this Agent you need to authenticate with Twitter in the [Services](/services) section first. |
@@ -5,6 +5,7 @@ module Agents |
||
5 | 5 |
cannot_receive_events! |
6 | 6 |
|
7 | 7 |
description <<-MD |
8 |
+ #{twitter_dependencies_missing if dependencies_missing?} |
|
8 | 9 |
The TwitterStreamAgent follows the Twitter stream in real time, watching for certain keywords, or filters, that you provide. |
9 | 10 |
|
10 | 11 |
To follow the Twitter stream, provide an array of `filters`. Multiple words in a filter must all show up in a tweet, but are independent of order. |
@@ -1,5 +1,3 @@ |
||
1 |
-require "twitter" |
|
2 |
- |
|
3 | 1 |
module Agents |
4 | 2 |
class TwitterUserAgent < Agent |
5 | 3 |
include TwitterConcern |
@@ -7,6 +5,7 @@ module Agents |
||
7 | 5 |
cannot_receive_events! |
8 | 6 |
|
9 | 7 |
description <<-MD |
8 |
+ #{twitter_dependencies_missing if dependencies_missing?} |
|
10 | 9 |
The TwitterUserAgent follows the timeline of a specified Twitter user. |
11 | 10 |
|
12 | 11 |
To be able to use this Agent you need to authenticate with Twitter in the [Services](/services) section first. |
@@ -65,8 +65,10 @@ module Agents |
||
65 | 65 |
private |
66 | 66 |
|
67 | 67 |
def handle_payload(payload) |
68 |
- if payload[:latitude].present? && payload[:longitude].present? |
|
69 |
- create_event payload: payload, lat: payload[:latitude].to_f, lng: payload[:longitude].to_f |
|
68 |
+ location = Location.new(payload) |
|
69 |
+ |
|
70 |
+ if location.present? |
|
71 |
+ create_event payload: payload, location: location |
|
70 | 72 |
end |
71 | 73 |
end |
72 | 74 |
end |
@@ -5,7 +5,10 @@ module Agents |
||
5 | 5 |
class WeatherAgent < Agent |
6 | 6 |
cannot_receive_events! |
7 | 7 |
|
8 |
+ gem_dependency_check { defined?(Wunderground) && defined?(ForecastIO) } |
|
9 |
+ |
|
8 | 10 |
description <<-MD |
11 |
+ #{'## Include `forecast_io` and `wunderground` in your Gemfile to use this Agent!' if dependencies_missing?} |
|
9 | 12 |
The WeatherAgent creates an event for the day's weather at a given `location`. |
10 | 13 |
|
11 | 14 |
You also must select `which_day` you would like to get the weather for where the number 0 is for today and 1 is for tomorrow and so on. Weather is only returned for 1 week at a time. |
@@ -14,7 +17,7 @@ module Agents |
||
14 | 17 |
|
15 | 18 |
The `location` can be a US zipcode, or any location that Wunderground supports. To find one, search [wunderground.com](http://wunderground.com) and copy the location part of the URL. For example, a result for San Francisco gives `http://www.wunderground.com/US/CA/San_Francisco.html` and London, England gives `http://www.wunderground.com/q/zmw:00000.1.03772`. The locations in each are `US/CA/San_Francisco` and `zmw:00000.1.03772`, respectively. |
16 | 19 |
|
17 |
- If you plan on using ForecastIO, the `location` must be a set of GPS coordinates. |
|
20 |
+ If you plan on using ForecastIO, the `location` must be a comma-separated string of co-ordinates (longitude, latitude). For example, San Francisco would be `37.7771,-122.4196`. |
|
18 | 21 |
|
19 | 22 |
You must setup an [API key for Wunderground](http://www.wunderground.com/weather/api/) in order to use this Agent with Wunderground. |
20 | 23 |
|
@@ -1,5 +1,4 @@ |
||
1 | 1 |
# encoding: utf-8 |
2 |
-require "weibo_2" |
|
3 | 2 |
|
4 | 3 |
module Agents |
5 | 4 |
class WeiboPublishAgent < Agent |
@@ -8,6 +7,7 @@ module Agents |
||
8 | 7 |
cannot_be_scheduled! |
9 | 8 |
|
10 | 9 |
description <<-MD |
10 |
+ #{'## Include `weibo_2` in your Gemfile to use this Agent!' if dependencies_missing?} |
|
11 | 11 |
The WeiboPublishAgent publishes tweets from the events it receives. |
12 | 12 |
|
13 | 13 |
You must first set up a Weibo app and generate an `acess_token` for the user to send statuses as. |
@@ -79,8 +79,7 @@ module Agents |
||
79 | 79 |
tweet_json[:entities][:urls].each do |url| |
80 | 80 |
text.gsub! url[:url], url[:expanded_url] |
81 | 81 |
end |
82 |
- return text |
|
82 |
+ text |
|
83 | 83 |
end |
84 |
- |
|
85 | 84 |
end |
86 | 85 |
end |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
# encoding: utf-8 |
2 |
-require "weibo_2" |
|
3 | 2 |
|
4 | 3 |
module Agents |
5 | 4 |
class WeiboUserAgent < Agent |
@@ -8,6 +7,7 @@ module Agents |
||
8 | 7 |
cannot_receive_events! |
9 | 8 |
|
10 | 9 |
description <<-MD |
10 |
+ #{'## Include `weibo_2` in your Gemfile to use this Agent!' if dependencies_missing?} |
|
11 | 11 |
The WeiboUserAgent follows the timeline of a specified Weibo user. It uses this endpoint: http://open.weibo.com/wiki/2/statuses/user_timeline/en |
12 | 12 |
|
13 | 13 |
You must first set up a Weibo app and generate an `acess_token` to authenticate with. Provide that, along with the `app_key` and `app_secret` for your Weibo app in the options. |
@@ -1,3 +1,5 @@ |
||
1 |
+require 'location' |
|
2 |
+ |
|
1 | 3 |
# Events are how Huginn Agents communicate and log information about the world. Events can be emitted and received by |
2 | 4 |
# Agents. They contain a serialized `payload` of arbitrary JSON data, as well as optional `lat`, `lng`, and `expires_at` |
3 | 5 |
# fields. |
@@ -5,7 +7,7 @@ class Event < ActiveRecord::Base |
||
5 | 7 |
include JSONSerializedField |
6 | 8 |
include LiquidDroppable |
7 | 9 |
|
8 |
- attr_accessible :lat, :lng, :payload, :user_id, :user, :expires_at |
|
10 |
+ attr_accessible :lat, :lng, :location, :payload, :user_id, :user, :expires_at |
|
9 | 11 |
|
10 | 12 |
acts_as_mappable |
11 | 13 |
|
@@ -28,6 +30,42 @@ class Event < ActiveRecord::Base |
||
28 | 30 |
where("expires_at IS NOT NULL AND expires_at < ?", Time.now) |
29 | 31 |
} |
30 | 32 |
|
33 |
+ scope :with_location, -> { |
|
34 |
+ where.not(lat: nil).where.not(lng: nil) |
|
35 |
+ } |
|
36 |
+ |
|
37 |
+ def location |
|
38 |
+ @location ||= Location.new( |
|
39 |
+ # lat and lng are BigDecimal, but converted to Float by the Location class |
|
40 |
+ lat: lat, |
|
41 |
+ lng: lng, |
|
42 |
+ radius: |
|
43 |
+ begin |
|
44 |
+ h = payload[:horizontal_accuracy].presence |
|
45 |
+ v = payload[:vertical_accuracy].presence |
|
46 |
+ if h && v |
|
47 |
+ (h.to_f + v.to_f) / 2 |
|
48 |
+ else |
|
49 |
+ (h || v || payload[:accuracy]).to_f |
|
50 |
+ end |
|
51 |
+ end, |
|
52 |
+ course: payload[:course], |
|
53 |
+ speed: payload[:speed].presence) |
|
54 |
+ end |
|
55 |
+ |
|
56 |
+ def location=(location) |
|
57 |
+ case location |
|
58 |
+ when nil |
|
59 |
+ self.lat = self.lng = nil |
|
60 |
+ return |
|
61 |
+ when Location |
|
62 |
+ else |
|
63 |
+ location = Location.new(location) |
|
64 |
+ end |
|
65 |
+ self.lat, self.lng = location.lat, location.lng |
|
66 |
+ location |
|
67 |
+ end |
|
68 |
+ |
|
31 | 69 |
# Emit this event again, as a new Event. |
32 | 70 |
def reemit! |
33 | 71 |
agent.create_event :payload => payload, :lat => lat, :lng => lng |
@@ -79,4 +117,8 @@ class EventDrop |
||
79 | 117 |
@object.created_at |
80 | 118 |
} |
81 | 119 |
end |
120 |
+ |
|
121 |
+ def _location_ |
|
122 |
+ @object.location |
|
123 |
+ end |
|
82 | 124 |
end |
@@ -1,5 +1,5 @@ |
||
1 | 1 |
<% if @agent.errors.any? %> |
2 |
- <div class="row well"> |
|
2 |
+ <div class="row well model-errors"> |
|
3 | 3 |
<h2><%= pluralize(@agent.errors.count, "error") %> prohibited this Agent from being saved:</h2> |
4 | 4 |
<% @agent.errors.full_messages.each do |msg| %> |
5 | 5 |
<p class='text-warning'><%= msg %></p> |
@@ -21,99 +21,103 @@ |
||
21 | 21 |
<% if @agent.new_record? %> |
22 | 22 |
<div class="form-group type-select"> |
23 | 23 |
<%= f.label :type %> |
24 |
- <%= f.select :type, options_for_select(Agent.types.map(&:to_s).sort.map {|type| [type.gsub(/^.*::/, ''), type] }, @agent.type), {}, :class => 'select2 form-control' %> |
|
24 |
+ <%= f.select :type, options_for_select([['Select an Agent Type', 'Agent']] + Agent.types.map(&:to_s).sort.map {|type| [type.gsub(/^.*::/, ''), type] }, @agent.type), {}, :class => 'select2 form-control' %> |
|
25 | 25 |
</div> |
26 | 26 |
<% end %> |
27 |
+ </div> |
|
27 | 28 |
|
28 |
- <div class="form-group type-select"> |
|
29 |
- <%= f.label :name %> |
|
30 |
- <%= f.text_field :name, :class => 'form-control' %> |
|
31 |
- </div> |
|
32 |
- |
|
33 |
- <div class='oauthable-form'> |
|
34 |
- <%= render partial: 'oauth_dropdown' %> |
|
35 |
- </div> |
|
29 |
+ <div class="agent-settings"> |
|
30 |
+ <div class="col-md-8"> |
|
31 |
+ <div class="form-group"> |
|
32 |
+ <%= f.label :name %> |
|
33 |
+ <%= f.text_field :name, :class => 'form-control' %> |
|
34 |
+ </div> |
|
36 | 35 |
|
37 |
- <div class="form-group"> |
|
38 |
- <%= f.label :schedule, :class => 'control-label' %> |
|
39 |
- <div class="schedule-region" data-can-be-scheduled="<%= @agent.can_be_scheduled? %>"> |
|
40 |
- <div class="can-be-scheduled"> |
|
41 |
- <%= f.select :schedule, options_for_select(Agent::SCHEDULES.map {|s| [s.humanize.titleize, s] }, @agent.schedule), {}, :class => 'form-control' %> |
|
42 |
- </div> |
|
43 |
- <span class='cannot-be-scheduled text-info'>This type of Agent cannot be scheduled.</span> |
|
36 |
+ <div class='oauthable-form'> |
|
37 |
+ <%= render partial: 'oauth_dropdown', locals: { agent: @agent } %> |
|
44 | 38 |
</div> |
45 |
- </div> |
|
46 | 39 |
|
47 |
- <div class="controller-region" data-has-controllers="<%= !@agent.controllers.empty? %>"> |
|
48 | 40 |
<div class="form-group"> |
49 |
- <%= f.label :controllers %> |
|
50 |
- <span class="glyphicon glyphicon-question-sign hover-help" data-content="Other than the system-defined schedule above, this agent may be run or controlled by these user-defined Agents."></span> |
|
51 |
- <div class="controller-list"> |
|
52 |
- <%= agent_controllers(@agent) || 'None' %> |
|
41 |
+ <%= f.label :schedule, :class => 'control-label' %> |
|
42 |
+ <div class="schedule-region" data-can-be-scheduled="<%= @agent.can_be_scheduled? %>"> |
|
43 |
+ <div class="can-be-scheduled"> |
|
44 |
+ <%= f.select :schedule, options_for_select(Agent::SCHEDULES.map {|s| [s.humanize.titleize, s] }, @agent.schedule), {}, :class => 'form-control' %> |
|
45 |
+ </div> |
|
46 |
+ <span class='cannot-be-scheduled text-info'>This type of Agent cannot be scheduled.</span> |
|
53 | 47 |
</div> |
54 | 48 |
</div> |
55 |
- </div> |
|
56 | 49 |
|
57 |
- <div class="control-link-region" data-can-control-other-agents="<%= @agent.can_control_other_agents? %>"> |
|
58 |
- <div class="can-control-other-agents"> |
|
50 |
+ <div class="controller-region" data-has-controllers="<%= !@agent.controllers.empty? %>"> |
|
59 | 51 |
<div class="form-group"> |
60 |
- <%= f.label :control_targets %> |
|
61 |
- <% eventControlTargets = current_user.agents.select(&:can_be_scheduled?) %> |
|
62 |
- <%= f.select(:control_target_ids, |
|
63 |
- options_for_select(eventControlTargets.map {|s| [s.name, s.id] }, |
|
64 |
- @agent.control_target_ids), |
|
65 |
- {}, { multiple: true, size: 5, class: 'select2 form-control' }) %> |
|
52 |
+ <%= f.label :controllers %> |
|
53 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="Other than the system-defined schedule above, this agent may be run or controlled by these user-defined Agents."></span> |
|
54 |
+ <div class="controller-list"> |
|
55 |
+ <%= agent_controllers(@agent) || 'None' %> |
|
56 |
+ </div> |
|
66 | 57 |
</div> |
67 | 58 |
</div> |
68 |
- </div> |
|
69 | 59 |
|
70 |
- <div class='event-related-region' data-can-create-events="<%= @agent.can_create_events? %>"> |
|
71 |
- <div class="form-group"> |
|
72 |
- <%= f.label :keep_events_for, "Keep events" %> |
|
73 |
- <span class="glyphicon glyphicon-question-sign hover-help" data-content="In order to conserve disk space, you can choose to have events created by this Agent expire after a certain period of time. Make sure you keep them long enough to allow any subsequent Agents to make use of them."></span> |
|
74 |
- <%= f.select :keep_events_for, options_for_select(Agent::EVENT_RETENTION_SCHEDULES, @agent.keep_events_for), {}, :class => 'form-control' %> |
|
60 |
+ <div class="control-link-region" data-can-control-other-agents="<%= @agent.can_control_other_agents? %>"> |
|
61 |
+ <div class="can-control-other-agents"> |
|
62 |
+ <div class="form-group"> |
|
63 |
+ <%= f.label :control_targets %> |
|
64 |
+ <% eventControlTargets = current_user.agents.select(&:can_be_scheduled?) %> |
|
65 |
+ <%= f.select(:control_target_ids, |
|
66 |
+ options_for_select(eventControlTargets.map {|s| [s.name, s.id] }, |
|
67 |
+ @agent.control_target_ids), |
|
68 |
+ {}, { multiple: true, size: 5, class: 'select2 form-control' }) %> |
|
69 |
+ </div> |
|
70 |
+ </div> |
|
75 | 71 |
</div> |
76 |
- </div> |
|
77 | 72 |
|
78 |
- <div class="form-group"> |
|
79 |
- <%= f.label :sources %> |
|
80 |
- <div class="link-region" data-can-receive-events="<%= @agent.can_receive_events? %>"> |
|
81 |
- <% eventSources = (current_user.agents - [@agent]).find_all { |a| a.can_create_events? } %> |
|
82 |
- <%= f.select(:source_ids, |
|
83 |
- options_for_select(eventSources.map {|s| [s.name, s.id] }, |
|
84 |
- @agent.source_ids), |
|
85 |
- {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %> |
|
86 |
- <span class='cannot-receive-events text-info'>This type of Agent cannot receive events.</span> |
|
87 |
- <%= f.label :propagate_immediately, :class => 'propagate-immediately' do %>Propagate immediately |
|
88 |
- <%= f.check_box :propagate_immediately %> |
|
89 |
- <% end %> |
|
73 |
+ <div class='event-related-region' data-can-create-events="<%= @agent.can_create_events? %>"> |
|
74 |
+ <div class="form-group"> |
|
75 |
+ <%= f.label :keep_events_for, "Keep events" %> |
|
76 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="In order to conserve disk space, you can choose to have events created by this Agent expire after a certain period of time. Make sure you keep them long enough to allow any subsequent Agents to make use of them."></span> |
|
77 |
+ <%= f.select :keep_events_for, options_for_select(Agent::EVENT_RETENTION_SCHEDULES, @agent.keep_events_for), {}, :class => 'form-control' %> |
|
78 |
+ </div> |
|
90 | 79 |
</div> |
91 |
- </div> |
|
92 | 80 |
|
93 |
- <% if current_user.scenario_count > 0 %> |
|
94 | 81 |
<div class="form-group"> |
95 |
- <%= f.label :scenarios %> |
|
96 |
- <span class="glyphicon glyphicon-question-sign hover-help" data-content="Use Scenarios to group sets of Agents, both for organization, and to make them easy to export and share."></span> |
|
97 |
- <%= f.select(:scenario_ids, |
|
98 |
- options_for_select(current_user.scenarios.pluck(:name, :id), @agent.scenario_ids), |
|
99 |
- {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %> |
|
82 |
+ <%= f.label :sources %> |
|
83 |
+ <div class="link-region" data-can-receive-events="<%= @agent.can_receive_events? %>"> |
|
84 |
+ <% eventSources = (current_user.agents - [@agent]).find_all { |a| a.can_create_events? } %> |
|
85 |
+ <%= f.select(:source_ids, |
|
86 |
+ options_for_select(eventSources.map {|s| [s.name, s.id] }, |
|
87 |
+ @agent.source_ids), |
|
88 |
+ {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %> |
|
89 |
+ <span class='cannot-receive-events text-info'>This type of Agent cannot receive events.</span> |
|
90 |
+ <%= f.label :propagate_immediately, :class => 'propagate-immediately' do %>Propagate immediately |
|
91 |
+ <%= f.check_box :propagate_immediately %> |
|
92 |
+ <% end %> |
|
93 |
+ </div> |
|
100 | 94 |
</div> |
101 |
- <% end %> |
|
102 | 95 |
|
103 |
- </div> |
|
96 |
+ <% if current_user.scenario_count > 0 %> |
|
97 |
+ <div class="form-group"> |
|
98 |
+ <%= f.label :scenarios %> |
|
99 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="Use Scenarios to group sets of Agents, both for organization, and to make them easy to export and share."></span> |
|
100 |
+ <%= f.select(:scenario_ids, |
|
101 |
+ options_for_select(current_user.scenarios.pluck(:name, :id), @agent.scenario_ids), |
|
102 |
+ {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %> |
|
103 |
+ </div> |
|
104 |
+ <% end %> |
|
104 | 105 |
|
105 |
- <!-- Form controls full width --> |
|
106 |
- <div class="col-md-12"> |
|
107 |
- <div class="form-group"> |
|
108 |
- <%= f.label :options %> |
|
109 |
- <span class="glyphicon glyphicon-question-sign hover-help" data-content="In this JSON hash, interpolation is available in almost all values using the Liquid templating language.<p>Available template variables include the following:<dl><dt><code>message</code>, <code>url</code>, etc.</dt><dd>Refers to the corresponding key's value of each incoming event's payload.</dd><dt><code>agent</code></dt><dd>Refers to the agent that created each incoming event. It has attributes like <code>type</code>, <code>name</code> and <code>options</code>, so <code>{{agent.type}}</code> will expand to <code>WebsiteAgent</code> if an incoming event is created by that agent.</dd></dl></p><p>To access user credentials, use the <code>credential</code> tag like this: <code>{% credential <em>bare_key_name</em> %}</code></p>"></span> |
|
110 |
- <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor <%= (@agent.new_record? && @agent.options == {}) ? "showing-default" : "" %>"> |
|
111 |
- <%= Utils.jsonify((@agent.new_record? && @agent.options == {}) ? @agent.default_options : @agent.options) %> |
|
112 |
- </textarea> |
|
113 | 106 |
</div> |
114 | 107 |
|
115 |
- <div class="form-group"> |
|
116 |
- <%= f.submit "Save", :class => "btn btn-primary" %> |
|
108 |
+ <!-- Form controls full width --> |
|
109 |
+ <div class="col-md-12"> |
|
110 |
+ <div class="form-group"> |
|
111 |
+ <%= f.label :options %> |
|
112 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="In this JSON hash, interpolation is available in almost all values using the Liquid templating language.<p>Available template variables include the following:<dl><dt><code>message</code>, <code>url</code>, etc.</dt><dd>Refers to the corresponding key's value of each incoming event's payload.</dd><dt><code>agent</code></dt><dd>Refers to the agent that created each incoming event. It has attributes like <code>type</code>, <code>name</code> and <code>options</code>, so <code>{{agent.type}}</code> will expand to <code>WebsiteAgent</code> if an incoming event is created by that agent.</dd></dl></p><p>To access user credentials, use the <code>credential</code> tag like this: <code>{% credential <em>bare_key_name</em> %}</code></p>"></span> |
|
113 |
+ <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor"> |
|
114 |
+ <%= Utils.jsonify((@agent.new_record? && @agent.options == {}) ? @agent.default_options : @agent.options) %> |
|
115 |
+ </textarea> |
|
116 |
+ </div> |
|
117 |
+ |
|
118 |
+ <div class="form-group"> |
|
119 |
+ <%= f.submit "Save", :class => "btn btn-primary" %> |
|
120 |
+ </div> |
|
117 | 121 |
</div> |
118 | 122 |
</div> |
119 | 123 |
</div> |
@@ -1,6 +1,6 @@ |
||
1 |
-<% if @agent.try(:oauthable?) %> |
|
1 |
+<% if agent.try(:oauthable?) %> |
|
2 | 2 |
<div class="form-group type-select"> |
3 | 3 |
<%= label_tag :service %> |
4 |
- <%= select_tag 'agent[service_id]', options_for_select(@agent.valid_services_for(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, @agent.service_id), class: 'form-control' %> |
|
4 |
+ <%= select_tag 'agent[service_id]', options_for_select(agent.valid_services_for(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, agent.service_id), class: 'form-control' %> |
|
5 | 5 |
</div> |
6 | 6 |
<% end %> |
@@ -13,7 +13,7 @@ |
||
13 | 13 |
|
14 | 14 |
<% @agents.each do |agent| %> |
15 | 15 |
<tr> |
16 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
16 |
+ <td class='<%= "agent-unavailable" if agent.unavailable? %>'> |
|
17 | 17 |
<%= link_to agent.name, agent_path(agent) %> |
18 | 18 |
<br/> |
19 | 19 |
<span class='text-muted'><%= agent.short_type.titleize %></span> |
@@ -23,35 +23,35 @@ |
||
23 | 23 |
</span> |
24 | 24 |
<% end %> |
25 | 25 |
</td> |
26 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
26 |
+ <td class='<%= "agent-unavailable" if agent.unavailable? %>'> |
|
27 | 27 |
<% if agent.can_be_scheduled? %> |
28 | 28 |
<%= agent_schedule(agent, ',<br/>') %> |
29 | 29 |
<% else %> |
30 | 30 |
<span class='not-applicable'></span> |
31 | 31 |
<% end %> |
32 | 32 |
</td> |
33 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
33 |
+ <td class='<%= "agent-unavailable" if agent.unavailable? %>'> |
|
34 | 34 |
<% if agent.can_be_scheduled? %> |
35 | 35 |
<%= agent.last_check_at ? time_ago_in_words(agent.last_check_at) + " ago" : "never" %> |
36 | 36 |
<% else %> |
37 | 37 |
<span class='not-applicable'></span> |
38 | 38 |
<% end %> |
39 | 39 |
</td> |
40 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
40 |
+ <td class='<%= "agent-unavailable" if agent.unavailable? %>'> |
|
41 | 41 |
<% if agent.can_create_events? %> |
42 | 42 |
<%= agent.last_event_at ? time_ago_in_words(agent.last_event_at) + " ago" : "never" %> |
43 | 43 |
<% else %> |
44 | 44 |
<span class='not-applicable'></span> |
45 | 45 |
<% end %> |
46 | 46 |
</td> |
47 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
47 |
+ <td class='<%= "agent-unavailable" if agent.unavailable? %>'> |
|
48 | 48 |
<% if agent.can_receive_events? %> |
49 | 49 |
<%= agent.last_receive_at ? time_ago_in_words(agent.last_receive_at) + " ago" : "never" %> |
50 | 50 |
<% else %> |
51 | 51 |
<span class='not-applicable'></span> |
52 | 52 |
<% end %> |
53 | 53 |
</td> |
54 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
54 |
+ <td class='<%= "agent-unavailable" if agent.unavailable? %>'> |
|
55 | 55 |
<% if agent.can_create_events? %> |
56 | 56 |
<%= link_to(agent.events_count || 0, agent_events_path(agent)) %> |
57 | 57 |
<% else %> |
@@ -1,8 +1,11 @@ |
||
1 |
-<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?sensor=false"></script> |
|
1 |
+<% content_for :head do -%> |
|
2 |
+<%= javascript_include_tag "https://maps.googleapis.com/maps/api/js?sensor=false" %> |
|
3 |
+<%= javascript_include_tag "map_marker" %> |
|
4 |
+<% end -%> |
|
2 | 5 |
|
3 | 6 |
<h3>Recent Event Map</h3> |
4 | 7 |
|
5 |
-<% events = @agent.events.where("lat IS NOT null AND lng IS NOT null").order("id desc").limit(500) %> |
|
8 |
+<% events = @agent.events.with_location.order("id desc").limit(500) %> |
|
6 | 9 |
<% if events.length > 0 %> |
7 | 10 |
<div id="map_canvas" style="width:800px; height:800px"></div> |
8 | 11 |
|
@@ -14,11 +17,10 @@ |
||
14 | 17 |
}; |
15 | 18 |
|
16 | 19 |
var map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions); |
20 |
+ <% events.each do |event| %> |
|
21 |
+ map_marker(map, <%= Utils.jsonify(event.location) %>); |
|
22 |
+ <% end %> |
|
17 | 23 |
</script> |
18 |
- |
|
19 |
- <% events.each do |event| %> |
|
20 |
- <%= render "shared/map_marker", event: event %> |
|
21 |
- <% end %> |
|
22 | 24 |
<% else %> |
23 | 25 |
<p> |
24 | 26 |
No events found. |
@@ -15,7 +15,7 @@ |
||
15 | 15 |
<ul> |
16 | 16 |
<li>Read <a href="https://github.com/cantino/huginn/wiki/Run-Huginn-for-free-on-Heroku" target="_target">this document</a> carefully if you are going to try out Huginn for free on <a href="https://id.heroku.com/" target="_target">Heroku</a>.</li> |
17 | 17 |
|
18 |
- <li>Install the <a href="https://toolbelt.heroku.com/" target="_target">Heroku Toolbelt</a> and run <kbd>heroku login</kbd> if you haven't already.</li> |
|
18 |
+ <li>Install the <a href="https://toolbelt.heroku.com/" target="_target">Heroku Toolbelt</a> and run <kbd>heroku login</kbd>, if you haven't already.</li> |
|
19 | 19 |
|
20 | 20 |
<li>Run the following commands:<br /> |
21 | 21 |
<%= content_tag :pre do -%> |
@@ -25,7 +25,7 @@ bundle |
||
25 | 25 |
bin/setup_heroku |
26 | 26 |
<%- end %> |
27 | 27 |
|
28 |
- <li>Get back to this page and sign up with the invitation code shown by the last command.</li> |
|
28 |
+ <li>This command will create an admin account for you.</li> |
|
29 | 29 |
</ul> |
30 | 30 |
</div> |
31 | 31 |
<% end %> |
@@ -78,4 +78,4 @@ bin/setup_heroku |
||
78 | 78 |
</div> |
79 | 79 |
</div> |
80 | 80 |
</div> |
81 |
-</div> |
|
81 |
+</div> |
@@ -16,7 +16,10 @@ |
||
16 | 16 |
</p> |
17 | 17 |
|
18 | 18 |
<% if @event.lat && @event.lng %> |
19 |
- <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?sensor=false"></script> |
|
19 |
+ <% content_for :head do -%> |
|
20 |
+<%= javascript_include_tag "https://maps.googleapis.com/maps/api/js?sensor=false" %> |
|
21 |
+<%= javascript_include_tag "map_marker" %> |
|
22 |
+ <% end -%> |
|
20 | 23 |
|
21 | 24 |
<p> |
22 | 25 |
<b>Lat:</b> |
@@ -36,9 +39,9 @@ |
||
36 | 39 |
}; |
37 | 40 |
|
38 | 41 |
var map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions); |
39 |
- </script> |
|
40 | 42 |
|
41 |
- <%= render "shared/map_marker", event: @event %> |
|
43 |
+ map_marker(map, <%= Utils.jsonify(@event.location) %>); |
|
44 |
+ </script> |
|
42 | 45 |
<% end %> |
43 | 46 |
|
44 | 47 |
<br /> |
@@ -28,7 +28,7 @@ |
||
28 | 28 |
|
29 | 29 |
<ul class="nav navbar-nav navbar-right"> |
30 | 30 |
<% if user_signed_in? %> |
31 |
- <form class="navbar-form navbar-left" role="search"> |
|
31 |
+ <form class="navbar-form navbar-left visible-lg" role="search"> |
|
32 | 32 |
<div class="form-group"> |
33 | 33 |
<input type="text" class="form-control" id='agent-navigate' autocomplete="off" placeholder="Search"> |
34 | 34 |
<%= image_tag "spinner-arrows.gif", :class => "spinner" %> |
@@ -36,22 +36,22 @@ |
||
36 | 36 |
</form> |
37 | 37 |
|
38 | 38 |
<li class='job-indicator' role='pending'> |
39 |
- <%= link_to current_user.admin? ? jobs_path : '#' do %> |
|
39 |
+ <%= link_to current_user.admin? ? jobs_path : '#', class: 'visible-lg' do %> |
|
40 | 40 |
<span class="badge"><span class="glyphicon glyphicon-refresh icon-white"></span> <span class='number'>0</span></span> |
41 | 41 |
<% end %> |
42 | 42 |
</li> |
43 | 43 |
<li class='job-indicator' role='awaiting_retry'> |
44 |
- <%= link_to current_user.admin? ? jobs_path : '#' do %> |
|
44 |
+ <%= link_to current_user.admin? ? jobs_path : '#', class: 'visible-lg' do %> |
|
45 | 45 |
<span class="badge"><span class="glyphicon glyphicon-question-sign icon-yellow"></span> <span class='number'>0</span></span> |
46 | 46 |
<% end %> |
47 | 47 |
</li> |
48 | 48 |
<li class='job-indicator' role='recent_failures'> |
49 |
- <%= link_to current_user.admin? ? jobs_path : '#' do %> |
|
49 |
+ <%= link_to current_user.admin? ? jobs_path : '#', class: 'hidden-sm hidden-xs' do %> |
|
50 | 50 |
<span class="badge"><span class="glyphicon glyphicon-exclamation-sign icon-white"></span> <span class='number'>0</span></span> |
51 | 51 |
<% end %> |
52 | 52 |
</li> |
53 | 53 |
<li id='event-indicator'> |
54 |
- <a href="#"> |
|
54 |
+ <a href="#" class='hidden-sm hidden-xs'> |
|
55 | 55 |
<span class="badge"><span class="glyphicon glyphicon-random icon-white"></span> <span class='number'>0</span> new events</span> |
56 | 56 |
</a> |
57 | 57 |
</li> |
@@ -35,22 +35,21 @@ |
||
35 | 35 |
</div> |
36 | 36 |
|
37 | 37 |
<script> |
38 |
- var agentPaths = {}; |
|
39 |
- var agentNames = []; |
|
38 |
+ window.agentPaths = {}; |
|
39 |
+ window.agentNames = []; |
|
40 | 40 |
<% if current_user.present? -%> |
41 | 41 |
var myAgents = <%= Utils.jsonify(current_user.agents.pluck(:name, :id).inject({}) {|m, a| m[a.first] = agent_path(a.last); m }) %>; |
42 | 42 |
var myScenarios = <%= Utils.jsonify(current_user.scenarios.pluck(:name, :id).inject({}) {|m, s| m[s.first + " Scenario"] = scenario_path(s.last); m }) %>; |
43 |
- $.extend(agentPaths, myAgents); |
|
44 |
- $.extend(agentPaths, myScenarios); |
|
45 |
- agentPaths["All Agents Index"] = <%= Utils.jsonify agents_path %>; |
|
46 |
- agentPaths["New Agent"] = <%= Utils.jsonify new_agent_path %>; |
|
47 |
- agentPaths["Account"] = <%= Utils.jsonify edit_user_registration_path %>; |
|
48 |
- agentPaths["Events Index"] = <%= Utils.jsonify events_path %>; |
|
49 |
- agentPaths["View Agent Diagram"] = <%= Utils.jsonify diagram_path %>; |
|
50 |
- agentPaths["Run Event Propagation"] = { url: <%= Utils.jsonify propagate_agents_path %>, method: 'POST' }; |
|
43 |
+ $.extend(window.agentPaths, myAgents); |
|
44 |
+ $.extend(window.agentPaths, myScenarios); |
|
45 |
+ window.agentPaths["All Agents Index"] = <%= Utils.jsonify agents_path %>; |
|
46 |
+ window.agentPaths["New Agent"] = <%= Utils.jsonify new_agent_path %>; |
|
47 |
+ window.agentPaths["Account"] = <%= Utils.jsonify edit_user_registration_path %>; |
|
48 |
+ window.agentPaths["Events Index"] = <%= Utils.jsonify events_path %>; |
|
49 |
+ window.agentPaths["View Agent Diagram"] = <%= Utils.jsonify diagram_path %>; |
|
50 |
+ window.agentPaths["Run Event Propagation"] = { url: <%= Utils.jsonify propagate_agents_path %>, method: 'POST' }; |
|
51 | 51 |
|
52 |
- |
|
53 |
- $.each(agentPaths, function(name, v) { agentNames.push(name); }); |
|
52 |
+ $.each(window.agentPaths, function(name, v) { window.agentNames.push(name); }); |
|
54 | 53 |
<% end -%> |
55 | 54 |
</script> |
56 | 55 |
</body> |
@@ -11,14 +11,14 @@ |
||
11 | 11 |
<%= link_to 'wiki', 'https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications', target: :_blank %> |
12 | 12 |
for guidance. |
13 | 13 |
</p> |
14 |
- <% if has_oauth_configuration_for('twitter') %> |
|
15 |
- <p><%= link_to "Authenticate with Twitter", "/auth/twitter" %></p> |
|
14 |
+ <% if has_oauth_configuration_for?('twitter') %> |
|
15 |
+ <p><%= link_to "/auth/twitter", class: 'btn btn-default btn-auth btn-auth-twitter' do %><i class='fa fa-twitter'></i><span>Authenticate with Twitter</span><% end %></p> |
|
16 | 16 |
<% end %> |
17 |
- <% if has_oauth_configuration_for('thirty_seven_signals') %> |
|
18 |
- <p><%= link_to "Authenticate with 37Signals (Basecamp)", "/auth/37signals" %></p> |
|
17 |
+ <% if has_oauth_configuration_for?('37signals') %> |
|
18 |
+ <p><%= link_to "/auth/37signals", class: 'btn btn-default btn-auth btn-auth-37signals' do %><i class='fa fa-lock'></i><span>Authenticate with 37Signals (Basecamp)</span><% end %></p> |
|
19 | 19 |
<% end -%> |
20 |
- <% if has_oauth_configuration_for('github') %> |
|
21 |
- <p><%= link_to "Authenticate with Github", "/auth/github" %></p> |
|
20 |
+ <% if has_oauth_configuration_for?('github') %> |
|
21 |
+ <p><%= link_to "/auth/github", class: 'btn btn-default btn-auth btn-auth-github' do %><i class='fa fa-github'></i><span>Authenticate with Github</span><% end %></p> |
|
22 | 22 |
<% end -%> |
23 | 23 |
<% if has_oauth_configuration_for('tumblr') %> |
24 | 24 |
<p><%= link_to "Authenticate with Tumblr", "/auth/tumblr" %></p> |
@@ -1,61 +0,0 @@ |
||
1 |
-<script> |
|
2 |
- (function(map) { |
|
3 |
- <% |
|
4 |
- if event.payload[:horizontal_accuracy] && event.payload[:vertical_accuracy] |
|
5 |
- radius = (event.payload[:horizontal_accuracy].to_f + event.payload[:vertical_accuracy].to_f) / 2.0 |
|
6 |
- elsif event.payload[:horizontal_accuracy] |
|
7 |
- radius = event.payload[:horizontal_accuracy].to_f |
|
8 |
- elsif event.payload[:vertical_accuracy] |
|
9 |
- radius = event.payload[:vertical_accuracy].to_f |
|
10 |
- elsif event.payload[:accuracy] |
|
11 |
- radius = event.payload[:accuracy].to_f |
|
12 |
- else |
|
13 |
- radius = 0 |
|
14 |
- end |
|
15 |
- %> |
|
16 |
- |
|
17 |
- var pos = new google.maps.LatLng(<%= event.lat %>, <%= event.lng %>); |
|
18 |
- |
|
19 |
- <% if radius > 0 %> |
|
20 |
- var accuracyCircle = new google.maps.Circle({ |
|
21 |
- strokeColor: '#FF0000', |
|
22 |
- strokeOpacity: 0.8, |
|
23 |
- strokeWeight: 2, |
|
24 |
- fillColor: '#FF0000', |
|
25 |
- fillOpacity: 0.35, |
|
26 |
- map: map, |
|
27 |
- center: pos, |
|
28 |
- radius: <%= radius %> |
|
29 |
- }); |
|
30 |
- <% else %> |
|
31 |
- var marker = new google.maps.Marker({ |
|
32 |
- position: pos, |
|
33 |
- map: map, |
|
34 |
- title: 'Recorded Location' |
|
35 |
- }); |
|
36 |
- <% end %> |
|
37 |
- |
|
38 |
- |
|
39 |
- <% if event.payload[:course] && event.payload[:course].to_f > -1 %> |
|
40 |
- var p1 = new LatLon(pos.lat(), pos.lng()); |
|
41 |
- var p2 = p1.destinationPoint(<%= event.payload[:course].to_f %>, <%= [0.2, (event.payload[:speed] || 1).to_f].max * 0.1 %>); |
|
42 |
- |
|
43 |
- var lineCoordinates = [ pos, new google.maps.LatLng(p2.lat(), p2.lon()) ]; |
|
44 |
- |
|
45 |
- var lineSymbol = { |
|
46 |
- path:google.maps.SymbolPath.FORWARD_CLOSED_ARROW |
|
47 |
- }; |
|
48 |
- |
|
49 |
- var line = new google.maps.Polyline({ |
|
50 |
- path: lineCoordinates, |
|
51 |
- icons: [ |
|
52 |
- { |
|
53 |
- icon: lineSymbol, |
|
54 |
- offset: '100%' |
|
55 |
- } |
|
56 |
- ], |
|
57 |
- map: map |
|
58 |
- }); |
|
59 |
- <% end %> |
|
60 |
- })(map); |
|
61 |
-</script> |
@@ -37,8 +37,9 @@ |
||
37 | 37 |
<br/> |
38 | 38 |
|
39 | 39 |
<div class="btn-group"> |
40 |
- <%= link_to '<span class="glyphicon glyphicon-plus"></span> New Credential'.html_safe, new_user_credential_path, class: "btn btn-default" %> |
|
40 |
+ <%= link_to new_user_credential_path, class: "btn btn-default" do %><span class="glyphicon glyphicon-plus"></span> New Credential<% end %> |
|
41 |
+ <%= link_to user_credentials_path(format: :json), class: "btn btn-default" do %><span class="glyphicon glyphicon-download-alt"></span> Download Credentials<% end %> |
|
41 | 42 |
</div> |
42 | 43 |
</div> |
43 | 44 |
</div> |
44 |
-</div> |
|
45 |
+</div> |
@@ -138,19 +138,6 @@ unless $config['SMTP_DOMAIN'] && $config['SMTP_USER_NAME'] && $config['SMTP_PASS |
||
138 | 138 |
end |
139 | 139 |
end |
140 | 140 |
|
141 |
-if first_time |
|
142 |
- puts "Restarting..." |
|
143 |
- puts capture("heroku restart") |
|
144 |
- |
|
145 |
- puts "Done!" |
|
146 |
- puts |
|
147 |
- puts "Visit https://#{app_name}.herokuapp.com/users/sign_up and use the invitation code shown below:" |
|
148 |
- puts |
|
149 |
- puts "\t#{$config['INVITATION_CODE']}" |
|
150 |
- |
|
151 |
- exit |
|
152 |
-end |
|
153 |
- |
|
154 | 141 |
branch = capture("git rev-parse --abbrev-ref HEAD") |
155 | 142 |
if yes?("Should I push your current branch (#{branch}) to heroku?") |
156 | 143 |
puts "This may take a moment..." |
@@ -158,21 +145,44 @@ if yes?("Should I push your current branch (#{branch}) to heroku?") |
||
158 | 145 |
|
159 | 146 |
puts "Running database migrations..." |
160 | 147 |
puts capture("heroku run rake db:migrate") |
148 |
+end |
|
161 | 149 |
|
150 |
+if first_time |
|
151 |
+ puts "Restarting..." |
|
152 |
+ puts capture("heroku restart") |
|
153 |
+ puts "Done!" |
|
162 | 154 |
puts |
163 | 155 |
puts |
164 | 156 |
puts "I can make an admin user on your new Huginn instance and setup some example Agents." |
165 | 157 |
if yes?("Should I create a new admin user and some example Agents?") |
166 |
- seed_email = nag "Okay, what is your email address?" |
|
167 |
- seed_username = nag "And what username would you like to login as?" |
|
168 |
- seed_password = nag "Finally, what password would you like to use?", noecho: true |
|
169 |
- puts "\nJust a moment..." |
|
170 |
- |
|
171 |
- capture("heroku run rake db:seed SEED_EMAIL=#{seed_email} SEED_USERNAME=#{seed_username} SEED_PASSWORD=#{seed_password}") |
|
158 |
+ done = false |
|
159 |
+ while !done |
|
160 |
+ seed_email = nag "Okay, what is your email address?" |
|
161 |
+ seed_username = nag "And what username would you like to login as?" |
|
162 |
+ seed_password = nag "Finally, what password would you like to use?", noecho: true |
|
163 |
+ puts "\nJust a moment..." |
|
164 |
+ |
|
165 |
+ result = capture("heroku run rake db:seed SEED_EMAIL=#{seed_email} SEED_USERNAME=#{seed_username} SEED_PASSWORD=#{seed_password}") |
|
166 |
+ if result =~ /Validation failed/ |
|
167 |
+ puts "ERROR:" |
|
168 |
+ puts |
|
169 |
+ puts result |
|
170 |
+ puts |
|
171 |
+ else |
|
172 |
+ done = true |
|
173 |
+ end |
|
174 |
+ end |
|
172 | 175 |
puts |
173 | 176 |
puts |
174 | 177 |
puts "Okay, you should be all set! Visit https://#{app_name}.herokuapp.com and login as '#{seed_username}' with your password." |
178 |
+ puts |
|
179 |
+ puts "If you'd like to make more users, you can visit https://#{app_name}.herokuapp.com/users/sign_up and use the invitation code:" |
|
180 |
+ else |
|
181 |
+ puts |
|
182 |
+ puts "Visit https://#{app_name}.herokuapp.com/users/sign_up and use the invitation code shown below:" |
|
175 | 183 |
end |
184 |
+ puts |
|
185 |
+ puts "\t#{$config['INVITATION_CODE']}" |
|
176 | 186 |
end |
177 | 187 |
|
178 | 188 |
puts |
@@ -1,6 +1,9 @@ |
||
1 | 1 |
require 'thread' |
2 | 2 |
require 'huginn_scheduler' |
3 | 3 |
|
4 |
+STDOUT.sync = true |
|
5 |
+STDERR.sync = true |
|
6 |
+ |
|
4 | 7 |
def stop |
5 | 8 |
puts 'Exiting...' |
6 | 9 |
@scheduler.stop |
@@ -14,6 +17,7 @@ def safely(&block) |
||
14 | 17 |
rescue StandardError => e |
15 | 18 |
STDERR.puts "\nException #{e.message}:\n#{e.backtrace.join("\n")}\n\n" |
16 | 19 |
STDERR.puts "Terminating myself ..." |
20 |
+ STDERR.flush |
|
17 | 21 |
stop |
18 | 22 |
end |
19 | 23 |
end |
@@ -61,7 +61,7 @@ Huginn::Application.configure do |
||
61 | 61 |
end |
62 | 62 |
|
63 | 63 |
# Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added) |
64 |
- config.assets.precompile += %w( diagram.js graphing.js user_credentials.js ) |
|
64 |
+ config.assets.precompile += %w( diagram.js graphing.js map_marker.js user_credentials.js ) |
|
65 | 65 |
|
66 | 66 |
# Ignore bad email addresses and do not raise email delivery errors. |
67 | 67 |
# Set this to true and configure the email server for immediate delivery to raise delivery errors. |
@@ -1,4 +1,4 @@ |
||
1 |
-unless Rails.env.test? |
|
1 |
+if defined?(RTurk) && !Rails.env.test? |
|
2 | 2 |
RTurk::logger.level = Logger::DEBUG |
3 | 3 |
RTurk.setup(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_ACCESS_KEY'], :sandbox => ENV['AWS_SANDBOX'] == "true") |
4 | 4 |
end |
@@ -1,6 +1,43 @@ |
||
1 |
+OMNIAUTH_PROVIDERS = {}.tap { |providers| |
|
2 |
+ if defined?(OmniAuth::Strategies::Twitter) && |
|
3 |
+ (key = ENV["TWITTER_OAUTH_KEY"]).present? && |
|
4 |
+ (secret = ENV["TWITTER_OAUTH_SECRET"]).present? |
|
5 |
+ providers['twitter'] = { |
|
6 |
+ omniauth_params: [key, secret, authorize_params: {force_login: 'true', use_authorize: 'true'}] |
|
7 |
+ } |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ if defined?(OmniAuth::Strategies::ThirtySevenSignals) && |
|
11 |
+ (key = ENV["THIRTY_SEVEN_SIGNALS_OAUTH_KEY"]).present? && |
|
12 |
+ (secret = ENV["THIRTY_SEVEN_SIGNALS_OAUTH_SECRET"]).present? |
|
13 |
+ providers['37signals'] = { |
|
14 |
+ omniauth_params: [key, secret] |
|
15 |
+ } |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ if defined?(OmniAuth::Strategies::GitHub) && |
|
19 |
+ (key = ENV["GITHUB_OAUTH_KEY"]).present? && |
|
20 |
+ (secret = ENV["GITHUB_OAUTH_SECRET"]).present? |
|
21 |
+ providers['github'] = { |
|
22 |
+ omniauth_params: [key, secret] |
|
23 |
+ } |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ if defined?(OmniAuth::Strategies::Tumblr) && |
|
27 |
+ (key = ENV["TUMBLR_OAUTH_KEY"]).present? && |
|
28 |
+ (secret = ENV["TUMBLR_OAUTH_SECRET"]).present? |
|
29 |
+ providers['tumblr'] = { |
|
30 |
+ omniauth_params: [key, secret] |
|
31 |
+ } |
|
32 |
+ end |
|
33 |
+} |
|
34 |
+ |
|
35 |
+def has_oauth_configuration_for?(provider) |
|
36 |
+ OMNIAUTH_PROVIDERS.key?(provider.to_s) |
|
37 |
+end |
|
38 |
+ |
|
1 | 39 |
Rails.application.config.middleware.use OmniAuth::Builder do |
2 |
- provider :twitter, ENV['TWITTER_OAUTH_KEY'], ENV['TWITTER_OAUTH_SECRET'], authorize_params: {force_login: 'true', use_authorize: 'true'} |
|
3 |
- provider '37signals', ENV['THIRTY_SEVEN_SIGNALS_OAUTH_KEY'], ENV['THIRTY_SEVEN_SIGNALS_OAUTH_SECRET'] |
|
4 |
- provider :github, ENV['GITHUB_OAUTH_KEY'], ENV['GITHUB_OAUTH_SECRET'] |
|
5 |
- provider :tumblr, ENV['TUMBLR_OAUTH_KEY'], ENV['TUMBLR_OAUTH_SECRET'] |
|
40 |
+ OMNIAUTH_PROVIDERS.each { |name, config| |
|
41 |
+ provider name, *config[:omniauth_params] |
|
42 |
+ } |
|
6 | 43 |
end |
@@ -0,0 +1,30 @@ |
||
1 |
+FROM ubuntu:14.04 |
|
2 |
+MAINTAINER Andrew Cantino |
|
3 |
+ |
|
4 |
+ENV DEBIAN_FRONTEND noninteractive |
|
5 |
+RUN apt-get update && \ |
|
6 |
+ apt-get install -y software-properties-common && \ |
|
7 |
+ add-apt-repository -y ppa:git-core/ppa && \ |
|
8 |
+ add-apt-repository -y ppa:brightbox/ruby-ng && \ |
|
9 |
+ apt-get update && \ |
|
10 |
+ apt-get install -y build-essential checkinstall postgresql-client \ |
|
11 |
+ git-core mysql-server redis-server python2.7 python-docutils \ |
|
12 |
+ libmysqlclient-dev libpq-dev zlib1g-dev libyaml-dev libssl-dev \ |
|
13 |
+ libgdbm-dev libreadline-dev libncurses5-dev libffi-dev \ |
|
14 |
+ libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev \ |
|
15 |
+ graphviz libgraphviz-dev \ |
|
16 |
+ ruby2.1 ruby2.1-dev supervisor && \ |
|
17 |
+ gem install --no-ri --no-rdoc bundler && \ |
|
18 |
+ rm -rf /var/lib/apt/lists/* |
|
19 |
+ |
|
20 |
+ADD scripts/ /scripts |
|
21 |
+RUN chmod 755 /scripts/setup /scripts/init |
|
22 |
+ |
|
23 |
+RUN /scripts/setup |
|
24 |
+ |
|
25 |
+VOLUME /var/lib/mysql |
|
26 |
+ |
|
27 |
+EXPOSE 5000 |
|
28 |
+ |
|
29 |
+CMD ["/scripts/init"] |
|
30 |
+ |
@@ -0,0 +1,2 @@ |
||
1 |
+build: |
|
2 |
+ docker build -t cantino/huginn . |
@@ -0,0 +1,137 @@ |
||
1 |
+Huginn for docker with multiple container linkage |
|
2 |
+================================================= |
|
3 |
+ |
|
4 |
+This image runs a linkable [Huginn](https://github.com/cantino/huginn) instance. |
|
5 |
+ |
|
6 |
+There is an automated build repository on docker hub for [cantino/huginn](https://registry.hub.docker.com/builds/github/cantino/huginn/). |
|
7 |
+ |
|
8 |
+This was patterned after [sameersbn/gitlab](https://registry.hub.docker.com/u/sameersbn/gitlab) by [ianblenke/huginn](http://github.com/ianblenke/huginn), and imported here for official generation of a docker hub auto-build image. |
|
9 |
+ |
|
10 |
+The scripts/init script generates a .env file containing the variables as passed as per normal Huginn documentation. |
|
11 |
+The same environment variables that would be used for Heroku PaaS deployment are used by this script. |
|
12 |
+ |
|
13 |
+The scripts/init script is aware of mysql and postgres linked containers through the environment variables: |
|
14 |
+ |
|
15 |
+ MYSQL_PORT_3306_TCP_ADDR |
|
16 |
+ MYSQL_PORT_3306_TCP_PORT |
|
17 |
+ |
|
18 |
+and |
|
19 |
+ |
|
20 |
+ POSTGRESQL_PORT_5432_TCP_ADDR |
|
21 |
+ POSTGRESQL_PORT_5432_TCP_PORT |
|
22 |
+ |
|
23 |
+Its recommended to use an image that allows you to create a database via environmental variables at docker run, like `paintedfox / postgresql` or `centurylink / mysql`, so the db is populated when this script runs. |
|
24 |
+ |
|
25 |
+If you do not link a database container, a built-in mysql database will be started. |
|
26 |
+There is an exported docker volume of /var/lib/mysql to allow persistence of that mysql database. |
|
27 |
+ |
|
28 |
+Additionally, the database variables may be overridden from the above as per the standard Huginn documentation: |
|
29 |
+ |
|
30 |
+ HUGINN_DATABASE_ADAPTER #(must be either 'postgres' or 'mysql2') |
|
31 |
+ HUGINN_DATABASE_HOST |
|
32 |
+ HUGINN_DATABASE_PORT |
|
33 |
+ |
|
34 |
+This script will run database migrations (rake db:migrate) which should be idempotent. |
|
35 |
+ |
|
36 |
+It will also seed the database (rake db:seed) unless this is defined: |
|
37 |
+ |
|
38 |
+ DO_NOT_SEED |
|
39 |
+ |
|
40 |
+This same seeding initially defines the "admin" user with a default password of "password" as per the standard Huginn documentation. |
|
41 |
+ |
|
42 |
+If you do not wish to have the default 6 agents, you will want to set the above environment variable after your initially deploy, otherwise they will be added automatically the next time a container pointing at the database is spun up. |
|
43 |
+ |
|
44 |
+The CMD launches Huginn via the scripts/init script. This may become the ENTRYPOINT later. It does take under a minute for Huginn to come up. Use environmental variables that match your DB's creds to ensure it works. |
|
45 |
+ |
|
46 |
+## Usage |
|
47 |
+ |
|
48 |
+Simple stand-alone usage: |
|
49 |
+ |
|
50 |
+ docker run -it -p 5000:5000 cantino/huginn |
|
51 |
+ |
|
52 |
+To link to another mysql container, for example: |
|
53 |
+ |
|
54 |
+ docker run --rm --name newcentury_mysql -p 3306 \ |
|
55 |
+ -e HUGINN_MYSQL_DATABASE=huginn \ |
|
56 |
+ -e HUGINN_MYSQL_USER=huginn \ |
|
57 |
+ -e HUGINN_MYSQL_PASSWORD=somethingsecret \ |
|
58 |
+ -e HUGINN_MYSQL_ROOT_PASSWORD=somethingevenmoresecret \ |
|
59 |
+ cantino/huginn |
|
60 |
+ docker run --rm --name huginn --link newcentury_mysql:MYSQL -p 5000:5000 \ |
|
61 |
+ -e HUGINN_DATABASE_NAME=huginn \ |
|
62 |
+ -e HUGINN_DATABASE_USER=huginn \ |
|
63 |
+ -e HUGINN_DATABASE_PASSWORD=somethingsecret \ |
|
64 |
+ cantino/huginn |
|
65 |
+ |
|
66 |
+To link to another container named 'postgres': |
|
67 |
+ |
|
68 |
+ docker run --rm --name huginn --link POSTGRES:mysql -p 5000:5000 -e "DATABASE_USER=huginn" -e "DATABASE_PASSWORD=pass@word" cantino/huginn |
|
69 |
+ |
|
70 |
+## Environment Variables |
|
71 |
+ |
|
72 |
+Other Huginn 12factored environment variables of note, as generated and put into the .env file as per Huginn documentation, |
|
73 |
+with an additional `HUGINN_` prefix to the variable. |
|
74 |
+ |
|
75 |
+These are: |
|
76 |
+ |
|
77 |
+ HUGINN_APP_SECRET_TOKEN |
|
78 |
+ HUGINN_DOMAIN |
|
79 |
+ HUGINN_ASSET_HOST |
|
80 |
+ HUGINN_DATABASE_ADAPTER |
|
81 |
+ HUGINN_DATABASE_ENCODING |
|
82 |
+ HUGINN_DATABASE_RECONNECT |
|
83 |
+ HUGINN_DATABASE_NAME |
|
84 |
+ HUGINN_DATABASE_POOL |
|
85 |
+ HUGINN_DATABASE_USERNAME |
|
86 |
+ HUGINN_DATABASE_PASSWORD |
|
87 |
+ HUGINN_DATABASE_HOST |
|
88 |
+ HUGINN_DATABASE_PORT |
|
89 |
+ HUGINN_DATABASE_SOCKET |
|
90 |
+ HUGINN_RAILS_ENV |
|
91 |
+ HUGINN_FORCE_SSL |
|
92 |
+ HUGINN_INVITATION_CODE |
|
93 |
+ HUGINN_SMTP_DOMAIM |
|
94 |
+ HUGINN_SMTP_USER_NAME |
|
95 |
+ HUGINN_SMTP_PASSWORD |
|
96 |
+ HUGINN_SMTP_SERVER |
|
97 |
+ HUGINN_SMTP_PORT |
|
98 |
+ HUGINN_SMTP_AUTHENTICATION |
|
99 |
+ HUGINN_SMTP_ENABLE_STARTTLS_AUTO |
|
100 |
+ HUGINN_EMAIL_FROM_ADDRESS |
|
101 |
+ HUGINN_AGENT_LOG_LENGTH |
|
102 |
+ HUGINN_TWITTER_OAUTH_KEY |
|
103 |
+ HUGINN_TWITTER_OAUTH_SECRET |
|
104 |
+ HUGINN_THIRTY_SEVEN_SIGNALS_OAUTH_KEY |
|
105 |
+ HUGINN_THIRTY_SEVEN_SIGNALS_OAUTH_SECRET |
|
106 |
+ HUGINN_GITHUB_OAUTH_KEY |
|
107 |
+ HUGINN_GITHUB_OAUTH_SECRET |
|
108 |
+ HUGINN_AWS_ACCESS_KEY_ID |
|
109 |
+ HUGINN_AWS_ACCESS_KEY |
|
110 |
+ HUGINN_AWS_SANDBOX |
|
111 |
+ HUGINN_FARADAY_HTTP_BACKEND |
|
112 |
+ HUGINN_DEFAULT_HTTP_USER_AGENT |
|
113 |
+ HUGINN_ALLOW_JSONPATH_EVAL |
|
114 |
+ HUGINN_ENABLE_INSECURE_AGENTS |
|
115 |
+ HUGGIN_ENABLE_SECOND_PRECISION_SCHEDULE |
|
116 |
+ HUGINN_USE_GRAPHVIZ_DOT |
|
117 |
+ HUGINN_TIMEZONE |
|
118 |
+ HUGGIN_FAILED_JOBS_TO_KEEP |
|
119 |
+ |
|
120 |
+ |
|
121 |
+The above environment variables will override the defaults. The defaults are read from the [.env.example](https://github.com/cantino/huginn/blob/master/.env.example) file. |
|
122 |
+ |
|
123 |
+For variables in the .env.example that are commented out, the default is to not include that variable in the generated .env file. |
|
124 |
+ |
|
125 |
+## Building on your own |
|
126 |
+ |
|
127 |
+You don't need to do this on your own, because there is an [automated build](https://registry.hub.docker.com/u/cantino/huginn/) for this repository, but if you really want: |
|
128 |
+ |
|
129 |
+ docker build --rm=true --tag={yourname}/huginn . |
|
130 |
+ |
|
131 |
+## Source |
|
132 |
+ |
|
133 |
+The source is [available on GitHub](https://github.com/cantino/huginn/). |
|
134 |
+ |
|
135 |
+Please feel free to submit pull requests and/or fork at your leisure. |
|
136 |
+ |
|
137 |
+ |
@@ -0,0 +1,111 @@ |
||
1 |
+#!/bin/bash |
|
2 |
+set -e |
|
3 |
+ |
|
4 |
+cd /app |
|
5 |
+ |
|
6 |
+# Default to the environment variable values set in .env.example |
|
7 |
+source /app/.env.example |
|
8 |
+ |
|
9 |
+# is a mysql or postgresql database linked? |
|
10 |
+# requires that the mysql or postgresql containers have exposed |
|
11 |
+# port 3306 and 5432 respectively. |
|
12 |
+if [ -n "${MYSQL_PORT_3306_TCP_ADDR}" ]; then |
|
13 |
+ HUGINN_DATABASE_ADAPTER=${HUGINN_DATABASE_ADAPTER:-mysql2} |
|
14 |
+ HUGINN_DATABASE_HOST=${HUGINN_DATABASE_HOST:-${MYSQL_PORT_3306_TCP_ADDR}} |
|
15 |
+ HUGINN_DATABASE_PORT=${HUGINN_DATABASE_PORT:-${MYSQL_PORT_3306_TCP_PORT}} |
|
16 |
+elif [ -n "${POSTGRESQL_PORT_5432_TCP_ADDR}" ]; then |
|
17 |
+ HUGINN_DATABASE_ADAPTER=${HUGINN_DATABASE_ADAPTER:-postgres} |
|
18 |
+ HUGINN_DATABASE_HOST=${HUGINN_DATABASE_HOST:-${POSTGRESQL_PORT_5432_TCP_ADDR}} |
|
19 |
+ HUGINN_DATABASE_PORT=${HUGINN_DATABASE_PORT:-${POSTGRESQL_PORT_5432_TCP_PORT}} |
|
20 |
+fi |
|
21 |
+ |
|
22 |
+grep = /app/.env.example | sed -e 's/^#[^ ]//' | grep -v -e '^#' | cut -d= -f1 | \ |
|
23 |
+ while read var ; do |
|
24 |
+ eval "echo \"$var=\\\"\${HUGINN_$var:-\$$var}\\\"\"" |
|
25 |
+ done | grep -v -e ^= > /app/.env |
|
26 |
+ |
|
27 |
+chmod ugo+r /app/.env |
|
28 |
+source /app/.env |
|
29 |
+ |
|
30 |
+DATABASE_HOST=${HUGINN_DATABASE_HOST:-${DATABASE_HOST:-localhost}} |
|
31 |
+DATABASE_ENCODING=${HUGINN_DATABASE_ENCODING:-${DATABASE_ENCODING}} |
|
32 |
+USE_GRAPHVIZ_DOT=${HUGINN_USE_GRAPHVIZ_DOT:-${USE_GRAPHVIZ_DOT}} |
|
33 |
+ |
|
34 |
+# use default port number if it is still not set |
|
35 |
+case "${DATABASE_ADAPTER}" in |
|
36 |
+ mysql2) DATABASE_PORT=${DATABASE_PORT:-3306} ;; |
|
37 |
+ postgres) DATABASE_PORT=${DATABASE_PORT:-5432} ;; |
|
38 |
+ *) echo "Unsupported database adapter. Available adapters are mysql2, and postgres." && exit 1 ;; |
|
39 |
+esac |
|
40 |
+ |
|
41 |
+# start supervisord |
|
42 |
+/usr/bin/supervisord -c /etc/supervisor/supervisord.conf |
|
43 |
+ |
|
44 |
+# start mysql server if ${DATABASE_HOST} is localhost |
|
45 |
+if [ "${DATABASE_HOST}" == "localhost" ]; then |
|
46 |
+ if [ "${DATABASE_ADAPTER}" == "postgres" ]; then |
|
47 |
+ echo "DATABASE_ADAPTER 'postgres' is not supported internally. Please provide DATABASE_HOST." |
|
48 |
+ exit 1 |
|
49 |
+ fi |
|
50 |
+ |
|
51 |
+ # configure supervisord to start mysql (manual) |
|
52 |
+ cat > /etc/supervisor/conf.d/mysqld.conf <<EOF |
|
53 |
+[program:mysqld] |
|
54 |
+priority=20 |
|
55 |
+directory=/tmp |
|
56 |
+command=/usr/bin/mysqld_safe |
|
57 |
+user=root |
|
58 |
+autostart=false |
|
59 |
+autorestart=true |
|
60 |
+stdout_logfile=/var/log/supervisor/%(program_name)s.log |
|
61 |
+stderr_logfile=/var/log/supervisor/%(program_name)s.log |
|
62 |
+EOF |
|
63 |
+ supervisorctl reload |
|
64 |
+ |
|
65 |
+ # fix permissions and ownership of /var/lib/mysql |
|
66 |
+ chown -R mysql:mysql /var/lib/mysql |
|
67 |
+ chmod 700 /var/lib/mysql |
|
68 |
+ |
|
69 |
+ # initialize MySQL data directory |
|
70 |
+ if [ ! -d /var/lib/mysql/mysql ]; then |
|
71 |
+ mysql_install_db --user=mysql |
|
72 |
+ fi |
|
73 |
+ |
|
74 |
+ echo "Starting mysql server..." |
|
75 |
+ supervisorctl start mysqld >/dev/null |
|
76 |
+ |
|
77 |
+ # wait for mysql server to start (max 120 seconds) |
|
78 |
+ timeout=120 |
|
79 |
+ while ! mysqladmin -uroot ${DATABASE_PASSWORD:+-p$DATABASE_PASSWORD} status >/dev/null 2>&1 |
|
80 |
+ do |
|
81 |
+ timeout=$(expr $timeout - 1) |
|
82 |
+ if [ $timeout -eq 0 ]; then |
|
83 |
+ echo "Failed to start mysql server" |
|
84 |
+ exit 1 |
|
85 |
+ fi |
|
86 |
+ sleep 1 |
|
87 |
+ done |
|
88 |
+ |
|
89 |
+ if ! echo "USE ${DATABASE_NAME}" | mysql -uroot ${DATABASE_PASSWORD:+-p$DATABASE_PASSWORD} >/dev/null 2>&1; then |
|
90 |
+ DB_INIT="yes" |
|
91 |
+ echo "CREATE DATABASE IF NOT EXISTS \`${DATABASE_NAME}\` DEFAULT CHARACTER SET \`utf8\` COLLATE \`utf8_unicode_ci\`;" | mysql -uroot ${DATABASE_PASSWORD:+-p$DATABASE_PASSWORD} |
|
92 |
+ echo "GRANT SELECT, LOCK TABLES, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER ON \`${DATABASE_NAME}\`.* TO 'root'@'localhost';" | mysql -uroot ${DATABASE_PASSWORD:+-p$DATABASE_PASSWORD} |
|
93 |
+ fi |
|
94 |
+fi |
|
95 |
+ |
|
96 |
+# Assuming we have a created database, run the migrations and seed it idempotently. |
|
97 |
+[ -z "${DO_NOT_MIGRATE}" ] && sudo -u huginn -EH bundle exec rake db:migrate |
|
98 |
+[ -z "${DO_NOT_SEED}" ] && sudo -u huginn -EH bundle exec rake db:seed |
|
99 |
+ |
|
100 |
+[ -n "$INTENTIONALLY_SLEEP" ] && sleep $INTENTIONALLY_SLEEP |
|
101 |
+ |
|
102 |
+# Fixup the Procfile and prepare the PORT |
|
103 |
+[ -z "${DO_NOT_RUN_JOBS}" ] && perl -pi -e 's/^jobs:/#jobs:/' /app/Procfile |
|
104 |
+perl -pi -e 's/rails server$/rails server -p \$PORT/' /app/Procfile |
|
105 |
+export PORT |
|
106 |
+ |
|
107 |
+# Start huginn |
|
108 |
+sudo -u huginn -EH bundle exec foreman start |
|
109 |
+ |
|
110 |
+# As the ENTRYPOINT script, when this exits the docker container will Exit. |
|
111 |
+exit 0 |
@@ -0,0 +1,39 @@ |
||
1 |
+#!/bin/bash |
|
2 |
+set -e |
|
3 |
+ |
|
4 |
+# Initialize variables used by Huginn at installation time |
|
5 |
+ |
|
6 |
+# Huginn is 12factor aware, embrace that fact for use inside of docker |
|
7 |
+ON_HEROKU=${ON_HEROKU:-true} |
|
8 |
+ |
|
9 |
+# Shallow clone the huginn project repo |
|
10 |
+git clone --depth 1 https://github.com/cantino/huginn /app |
|
11 |
+ |
|
12 |
+cd /app |
|
13 |
+ |
|
14 |
+# add a huginn group and user |
|
15 |
+adduser --group huginn |
|
16 |
+adduser --disabled-login --ingroup huginn --gecos 'Huginn' --no-create-home --home /app huginn |
|
17 |
+adduser huginn sudo |
|
18 |
+passwd -d huginn |
|
19 |
+ |
|
20 |
+# Change the ownership to huginn |
|
21 |
+chown -R huginn:huginn /app |
|
22 |
+ |
|
23 |
+# create required tmp and log directories |
|
24 |
+sudo -u huginn -H mkdir -p tmp/pids tmp/cache tmp/sockets log |
|
25 |
+chmod -R u+rwX log tmp |
|
26 |
+ |
|
27 |
+# install gems required by Huginn, use local cache if available |
|
28 |
+if [ -d "/scripts/cache" ]; then |
|
29 |
+ mv /scripts/cache vendor/ |
|
30 |
+ chown -R huginn:huginn vendor/cache |
|
31 |
+fi |
|
32 |
+sudo -u huginn -H bundle install --deployment --without development test |
|
33 |
+ |
|
34 |
+# silence setlocale message (THANKS DEBIAN!) |
|
35 |
+cat > /etc/default/locale <<EOF |
|
36 |
+LC_ALL=en_US.UTF-8 |
|
37 |
+LANG=en_US.UTF-8 |
|
38 |
+EOF |
|
39 |
+ |
@@ -40,7 +40,7 @@ class Rufus::Scheduler |
||
40 | 40 |
def schedule_scheduler_agent(agent) |
41 | 41 |
job = scheduler_agent_job(agent) |
42 | 42 |
|
43 |
- if agent.disabled? |
|
43 |
+ if agent.unavailable? |
|
44 | 44 |
if job |
45 | 45 |
puts "Unscheduling SchedulerAgent##{agent.id} (disabled)" |
46 | 46 |
job.unschedule |
@@ -0,0 +1,110 @@ |
||
1 |
+require 'liquid' |
|
2 |
+ |
|
3 |
+Location = Struct.new(:lat, :lng, :radius, :speed, :course) |
|
4 |
+ |
|
5 |
+class Location |
|
6 |
+ include LiquidDroppable |
|
7 |
+ |
|
8 |
+ protected :[]= |
|
9 |
+ |
|
10 |
+ def initialize(data = {}) |
|
11 |
+ super() |
|
12 |
+ |
|
13 |
+ case data |
|
14 |
+ when Array |
|
15 |
+ raise ArgumentError, 'unsupported location data' unless data.size == 2 |
|
16 |
+ self.lat, self.lng = data |
|
17 |
+ when Hash, Location |
|
18 |
+ data.each { |key, value| |
|
19 |
+ case key.to_sym |
|
20 |
+ when :lat, :latitude |
|
21 |
+ self.lat = value |
|
22 |
+ when :lng, :longitude |
|
23 |
+ self.lng = value |
|
24 |
+ when :radius |
|
25 |
+ self.radius = value |
|
26 |
+ when :speed |
|
27 |
+ self.speed = value |
|
28 |
+ when :course |
|
29 |
+ self.course = value |
|
30 |
+ end |
|
31 |
+ } |
|
32 |
+ else |
|
33 |
+ raise ArgumentError, 'unsupported location data' |
|
34 |
+ end |
|
35 |
+ |
|
36 |
+ yield self if block_given? |
|
37 |
+ end |
|
38 |
+ |
|
39 |
+ def lat=(value) |
|
40 |
+ self[:lat] = floatify(value) { |f| |
|
41 |
+ if f.abs <= 90 |
|
42 |
+ f |
|
43 |
+ else |
|
44 |
+ raise ArgumentError, 'out of bounds' |
|
45 |
+ end |
|
46 |
+ } |
|
47 |
+ end |
|
48 |
+ |
|
49 |
+ alias latitude lat |
|
50 |
+ alias latitude= lat= |
|
51 |
+ |
|
52 |
+ def lng=(value) |
|
53 |
+ self[:lng] = floatify(value) { |f| |
|
54 |
+ if f.abs <= 180 |
|
55 |
+ f |
|
56 |
+ else |
|
57 |
+ raise ArgumentError, 'out of bounds' |
|
58 |
+ end |
|
59 |
+ } |
|
60 |
+ end |
|
61 |
+ |
|
62 |
+ alias longitude lng |
|
63 |
+ alias longitude= lng= |
|
64 |
+ |
|
65 |
+ def radius=(value) |
|
66 |
+ self[:radius] = floatify(value) { |f| f if f >= 0 } |
|
67 |
+ end |
|
68 |
+ |
|
69 |
+ def speed=(value) |
|
70 |
+ self[:speed] = floatify(value) { |f| f if f >= 0 } |
|
71 |
+ end |
|
72 |
+ |
|
73 |
+ def course=(value) |
|
74 |
+ self[:course] = floatify(value) { |f| f if (0..360).cover?(f) } |
|
75 |
+ end |
|
76 |
+ |
|
77 |
+ def present? |
|
78 |
+ lat && lng |
|
79 |
+ end |
|
80 |
+ |
|
81 |
+ def empty? |
|
82 |
+ !present? |
|
83 |
+ end |
|
84 |
+ |
|
85 |
+ private |
|
86 |
+ |
|
87 |
+ def floatify(value) |
|
88 |
+ case value |
|
89 |
+ when nil, '' |
|
90 |
+ return nil |
|
91 |
+ else |
|
92 |
+ float = Float(value) |
|
93 |
+ if block_given? |
|
94 |
+ yield(float) |
|
95 |
+ else |
|
96 |
+ float |
|
97 |
+ end |
|
98 |
+ end |
|
99 |
+ end |
|
100 |
+end |
|
101 |
+ |
|
102 |
+class LocationDrop |
|
103 |
+ KEYS = Location.members.map(&:to_s).concat(%w[latitude longitude]) |
|
104 |
+ |
|
105 |
+ def before_method(key) |
|
106 |
+ if KEYS.include?(key) |
|
107 |
+ @object.__send__(key) |
|
108 |
+ end |
|
109 |
+ end |
|
110 |
+end |
@@ -1,6 +1,5 @@ |
||
1 | 1 |
require 'cgi' |
2 | 2 |
require 'json' |
3 |
-require 'twitter/json_stream' |
|
4 | 3 |
require 'em-http-request' |
5 | 4 |
require 'pp' |
6 | 5 |
|
@@ -88,6 +87,14 @@ class TwitterStream |
||
88 | 87 |
SEPARATOR = /[^\w_\-]+/ |
89 | 88 |
|
90 | 89 |
def run |
90 |
+ if Agents::TwitterStreamAgent.dependencies_missing? |
|
91 |
+ STDERR.puts Agents::TwitterStreamAgent.twitter_dependencies_missing |
|
92 |
+ STDERR.flush |
|
93 |
+ return |
|
94 |
+ end |
|
95 |
+ |
|
96 |
+ require 'twitter/json_stream' |
|
97 |
+ |
|
91 | 98 |
while @running |
92 | 99 |
begin |
93 | 100 |
agents = Agents::TwitterStreamAgent.active.all |
@@ -0,0 +1,68 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Location do |
|
4 |
+ let(:location) { |
|
5 |
+ Location.new( |
|
6 |
+ lat: BigDecimal.new('2.0'), |
|
7 |
+ lng: BigDecimal.new('3.0'), |
|
8 |
+ radius: 300, |
|
9 |
+ speed: 2, |
|
10 |
+ course: 30) |
|
11 |
+ } |
|
12 |
+ |
|
13 |
+ it "converts values to Float" do |
|
14 |
+ expect(location.lat).to be_a Float |
|
15 |
+ expect(location.lat).to eq 2.0 |
|
16 |
+ expect(location.lng).to be_a Float |
|
17 |
+ expect(location.lng).to eq 3.0 |
|
18 |
+ expect(location.radius).to be_a Float |
|
19 |
+ expect(location.radius).to eq 300.0 |
|
20 |
+ expect(location.speed).to be_a Float |
|
21 |
+ expect(location.speed).to eq 2.0 |
|
22 |
+ expect(location.course).to be_a Float |
|
23 |
+ expect(location.course).to eq 30.0 |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ it "provides hash-style access to its properties with both symbol and string keys" do |
|
27 |
+ expect(location[:lat]).to be_a Float |
|
28 |
+ expect(location[:lat]).to eq 2.0 |
|
29 |
+ expect(location['lat']).to be_a Float |
|
30 |
+ expect(location['lat']).to eq 2.0 |
|
31 |
+ end |
|
32 |
+ |
|
33 |
+ it "does not allow hash-style assignment" do |
|
34 |
+ expect { |
|
35 |
+ location[:lat] = 2.0 |
|
36 |
+ }.to raise_error |
|
37 |
+ end |
|
38 |
+ |
|
39 |
+ it "ignores invalid values" do |
|
40 |
+ location2 = Location.new( |
|
41 |
+ lat: 2, |
|
42 |
+ lng: 3, |
|
43 |
+ radius: -1, |
|
44 |
+ speed: -1, |
|
45 |
+ course: -1) |
|
46 |
+ expect(location2.radius).to be_nil |
|
47 |
+ expect(location2.speed).to be_nil |
|
48 |
+ expect(location2.course).to be_nil |
|
49 |
+ end |
|
50 |
+ |
|
51 |
+ it "considers a location empty if either latitude or longitude is missing" do |
|
52 |
+ expect(Location.new.empty?).to be_truthy |
|
53 |
+ expect(Location.new(lat: 2, radius: 1).present?).to be_falsy |
|
54 |
+ expect(Location.new(lng: 3, radius: 1).present?).to be_falsy |
|
55 |
+ end |
|
56 |
+ |
|
57 |
+ it "is droppable" do |
|
58 |
+ { |
|
59 |
+ '{{location.lat}}' => '2.0', |
|
60 |
+ '{{location.latitude}}' => '2.0', |
|
61 |
+ '{{location.lng}}' => '3.0', |
|
62 |
+ '{{location.longitude}}' => '3.0', |
|
63 |
+ }.each { |template, result| |
|
64 |
+ expect(Liquid::Template.parse(template).render('location' => location.to_liquid)).to eq(result), |
|
65 |
+ "expected #{template.inspect} to expand to #{result.inspect}" |
|
66 |
+ } |
|
67 |
+ end |
|
68 |
+end |
@@ -1,6 +1,50 @@ |
||
1 | 1 |
require 'spec_helper' |
2 | 2 |
|
3 | 3 |
describe Event do |
4 |
+ describe ".with_location" do |
|
5 |
+ it "selects events with location" do |
|
6 |
+ event = events(:bob_website_agent_event) |
|
7 |
+ event.lat = 2 |
|
8 |
+ event.lng = 3 |
|
9 |
+ event.save! |
|
10 |
+ Event.with_location.pluck(:id).should == [event.id] |
|
11 |
+ |
|
12 |
+ event.lat = nil |
|
13 |
+ event.save! |
|
14 |
+ Event.with_location.should be_empty |
|
15 |
+ end |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ describe "#location" do |
|
19 |
+ it "returns a default hash when an event does not have a location" do |
|
20 |
+ event = events(:bob_website_agent_event) |
|
21 |
+ event.location.should == Location.new( |
|
22 |
+ lat: nil, |
|
23 |
+ lng: nil, |
|
24 |
+ radius: 0.0, |
|
25 |
+ speed: nil, |
|
26 |
+ course: nil) |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ it "returns a hash containing location information" do |
|
30 |
+ event = events(:bob_website_agent_event) |
|
31 |
+ event.lat = 2 |
|
32 |
+ event.lng = 3 |
|
33 |
+ event.payload = { |
|
34 |
+ radius: 300, |
|
35 |
+ speed: 0.5, |
|
36 |
+ course: 90.0, |
|
37 |
+ } |
|
38 |
+ event.save! |
|
39 |
+ event.location.should == Location.new( |
|
40 |
+ lat: 2.0, |
|
41 |
+ lng: 3.0, |
|
42 |
+ radius: 0.0, |
|
43 |
+ speed: 0.5, |
|
44 |
+ course: 90.0) |
|
45 |
+ end |
|
46 |
+ end |
|
47 |
+ |
|
4 | 48 |
describe "#reemit" do |
5 | 49 |
it "creates a new event identical to itself" do |
6 | 50 |
events(:bob_website_agent_event).lat = 2 |
@@ -130,6 +174,8 @@ describe EventDrop do |
||
130 | 174 |
'title' => 'some title', |
131 | 175 |
'url' => 'http://some.site.example.org/', |
132 | 176 |
} |
177 |
+ @event.lat = 2 |
|
178 |
+ @event.lng = 3 |
|
133 | 179 |
@event.save! |
134 | 180 |
end |
135 | 181 |
|
@@ -166,4 +212,9 @@ describe EventDrop do |
||
166 | 212 |
t = '{{created_at | date:"%FT%T%z" }}' |
167 | 213 |
interpolate(t, @event).should eq(@event.created_at.strftime("%FT%T%z")) |
168 | 214 |
end |
215 |
+ |
|
216 |
+ it 'should have _location_' do |
|
217 |
+ t = '{{_location_.lat}},{{_location_.lng}}' |
|
218 |
+ interpolate(t, @event).should eq("2.0,3.0") |
|
219 |
+ end |
|
169 | 220 |
end |
@@ -1,3 +1,4 @@ |
||
1 |
+ |
|
1 | 2 |
/* |
2 | 3 |
Copyright (c) 2014, Andrew Cantino |
3 | 4 |
Copyright (c) 2009, Andrew Cantino & Kyle Maxwell |
@@ -528,7 +529,7 @@ |
||
528 | 529 |
} |
529 | 530 |
innerbq.append($('<span class="colon">: </span>')); |
530 | 531 |
newElem = this.build(jsonvalue, innerbq, json, jsonkey, root); |
531 |
- if (newElem && newElem.text() === "??") { |
|
532 |
+ if (!elem && newElem && newElem.text() === "??") { |
|
532 | 533 |
elem = newElem; |
533 | 534 |
} |
534 | 535 |
bq.append(innerbq); |